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
@@ -0,0 +1,568 @@
|
||||
"""
|
||||
编译验证插件 - 批量语法/编译检查
|
||||
对项目内 Python 文件做 ast.parse + py_compile 双重验证,
|
||||
支持按目录、单文件或全项目扫描,并发执行加速。
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import ast
|
||||
import time
|
||||
import shutil
|
||||
import py_compile
|
||||
import concurrent.futures
|
||||
from pathlib import Path
|
||||
from typing import List, Tuple, Optional
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
from xcli_core.tool_result import ToolResult, ErrorCode
|
||||
|
||||
try:
|
||||
from colorama import Fore, Style, init as _colorama_init
|
||||
_colorama_init(autoreset=False)
|
||||
_HAS_COLOR = True
|
||||
except Exception:
|
||||
_HAS_COLOR = False
|
||||
|
||||
class _Stub:
|
||||
def __getattr__(self, _): return ""
|
||||
Fore = Style = _Stub()
|
||||
|
||||
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
SKIP_DIRS = {
|
||||
"__pycache__", ".git", ".venv", "venv", "env", ".env",
|
||||
"node_modules", ".idea", ".vscode", "dist", "build",
|
||||
".mypy_cache", ".pytest_cache", ".ruff_cache", "site-packages",
|
||||
}
|
||||
|
||||
SKIP_FILES = {
|
||||
"setup.py",
|
||||
}
|
||||
|
||||
|
||||
class Liugin:
|
||||
"""编译验证 - 批量语法检查 / 编译验证 / 清理 __pycache__"""
|
||||
|
||||
def __init__(self):
|
||||
self.usage = """编译验证工具
|
||||
|
||||
操作:
|
||||
all - 验证项目内全部 Python 文件
|
||||
core - 仅验证 xcli_core/ 目录
|
||||
engines - 仅验证 ai_engines/ 目录
|
||||
plugins - 仅验证 plugins/ 目录
|
||||
dir <目录> - 验证指定目录
|
||||
check <文件> - 验证单个文件 (可多次指定)
|
||||
clean [目录] - 清理 __pycache__ (默认项目根)
|
||||
stats - 统计各目录 Python 文件数量
|
||||
|
||||
选项:
|
||||
-v / --verbose - 显示每个文件的结果
|
||||
-j / --jobs <N> - 并发线程数 (默认 8)
|
||||
-q / --quiet - 仅在结束时打印汇总
|
||||
|
||||
输出:
|
||||
✅ 绿色 = 通过
|
||||
❌ 红色 = 失败
|
||||
汇总: 总数/通过/失败 + 失败文件详情
|
||||
|
||||
示例:
|
||||
check all -v
|
||||
check core
|
||||
check plugins/compile_check.py
|
||||
check dir ai_engines -j 16
|
||||
check clean
|
||||
"""
|
||||
self.cli = None
|
||||
|
||||
def set_cli(self, cli):
|
||||
self.cli = cli
|
||||
|
||||
def get_tool_info(self):
|
||||
return {
|
||||
"name": "compile_check",
|
||||
"description": "编译验证工具 — 批量语法/编译检查(ast.parse + py_compile),支持目录、单文件、全项目扫描,并发加速,清理 __pycache__",
|
||||
"keywords": ["编译", "验证", "语法", "检查", "compile", "syntax",
|
||||
"check", "lint", "py_compile", "ast", "批量",
|
||||
"validate", "verify", "clean", "pycache"],
|
||||
"usage": self.usage,
|
||||
}
|
||||
|
||||
def get_mcp_definition(self):
|
||||
return {
|
||||
"name": "compile_check",
|
||||
"description": "编译验证工具",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"operation": {
|
||||
"type": "string",
|
||||
"enum": ["all", "core", "engines", "plugins",
|
||||
"dir", "check", "clean", "stats"],
|
||||
"description": "操作类型"
|
||||
},
|
||||
"path": {"type": "string",
|
||||
"description": "目录或文件路径"},
|
||||
"paths": {"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "多文件路径列表"},
|
||||
"verbose": {"type": "boolean", "default": False},
|
||||
"jobs": {"type": "integer", "default": 8},
|
||||
"quiet": {"type": "boolean", "default": False},
|
||||
},
|
||||
"required": ["operation"]
|
||||
}
|
||||
}
|
||||
|
||||
def convert_mcp_args(self, arguments):
|
||||
op = arguments.get("operation", "")
|
||||
path = arguments.get("path", "")
|
||||
return f"{op} {path}".strip()
|
||||
|
||||
def handle(self, args: str):
|
||||
try:
|
||||
parts = self._parse_args(args)
|
||||
if not parts:
|
||||
return self._print_usage()
|
||||
|
||||
operation = parts[0].lower()
|
||||
opts = self._extract_options(parts[1:])
|
||||
|
||||
if operation in ("help", "-h", "--help"):
|
||||
return self._print_usage()
|
||||
|
||||
routes = {
|
||||
"all": self._op_all,
|
||||
"core": self._op_core,
|
||||
"engines": self._op_engines,
|
||||
"plugins": self._op_plugins,
|
||||
"dir": self._op_dir,
|
||||
"check": self._op_check,
|
||||
"clean": self._op_clean,
|
||||
"stats": self._op_stats,
|
||||
}
|
||||
handler = routes.get(operation)
|
||||
if not handler:
|
||||
return ToolResult.fail(
|
||||
f"不支持的操作 '{operation}'",
|
||||
ErrorCode.UNSUPPORTED_OP,
|
||||
tool_name="compile_check",
|
||||
)
|
||||
return handler(opts)
|
||||
|
||||
except Exception as e:
|
||||
return ToolResult.fail(
|
||||
f"编译验证错误: {e}",
|
||||
ErrorCode.EXEC_FAILED,
|
||||
tool_name="compile_check",
|
||||
)
|
||||
|
||||
def _print_usage(self):
|
||||
self._emit(self.usage)
|
||||
return ToolResult.ok("usage_printed", tool_name="compile_check")
|
||||
|
||||
def _out(self, msg: str):
|
||||
"""统一输出,适配 TUI/CLI"""
|
||||
if self.cli is not None and hasattr(self.cli, "_output"):
|
||||
self.cli._output(msg)
|
||||
else:
|
||||
print(msg)
|
||||
|
||||
def _emit(self, msg: str, end: str = "\n"):
|
||||
"""统一输出,自动适配 TUI/CLI
|
||||
|
||||
- TUI 模式: 一行一行输出到 _output,忽略 end(TUI 有自己的状态指示器)
|
||||
- CLI 模式: 尊重 end,支持 end="" 的进度条刷新
|
||||
- TUI 模式下 end != "\\n" 的进度条输出直接跳过(避免污染对话区)
|
||||
"""
|
||||
if self.cli is not None and hasattr(self.cli, "_output"):
|
||||
if end != "\n":
|
||||
return # TUI 模式跳过进度条类输出
|
||||
self.cli._output(msg)
|
||||
else:
|
||||
print(msg, end=end)
|
||||
|
||||
def _ok(self, msg: str) -> str:
|
||||
return f"{Fore.GREEN}{msg}{Style.RESET_ALL}"
|
||||
|
||||
def _err(self, msg: str) -> str:
|
||||
return f"{Fore.RED}{msg}{Style.RESET_ALL}"
|
||||
|
||||
def _warn(self, msg: str) -> str:
|
||||
return f"{Fore.YELLOW}{msg}{Style.RESET_ALL}"
|
||||
|
||||
def _dim(self, msg: str) -> str:
|
||||
return f"{Fore.CYAN}{msg}{Style.RESET_ALL}"
|
||||
|
||||
def _banner(self, title: str) -> str:
|
||||
bar = "═" * max(20, len(title) + 4)
|
||||
return f"{Fore.CYAN}{bar}\n {title}\n{bar}{Style.RESET_ALL}"
|
||||
|
||||
def _parse_args(self, args: str) -> List[str]:
|
||||
parts, current, in_quotes, qc = [], "", False, None
|
||||
for ch in args:
|
||||
if ch in ('"', "'") and not in_quotes:
|
||||
in_quotes, qc = True, ch
|
||||
elif ch == qc and in_quotes:
|
||||
in_quotes, qc = False, None
|
||||
elif ch == " " and not in_quotes:
|
||||
if current:
|
||||
parts.append(current)
|
||||
current = ""
|
||||
else:
|
||||
current += ch
|
||||
if current:
|
||||
parts.append(current)
|
||||
return parts
|
||||
|
||||
def _extract_options(self, args: List[str]) -> dict:
|
||||
opts = {
|
||||
"positional": [],
|
||||
"verbose": False,
|
||||
"quiet": False,
|
||||
"jobs": 8,
|
||||
}
|
||||
i = 0
|
||||
while i < len(args):
|
||||
a = args[i]
|
||||
if a in ("-v", "--verbose"):
|
||||
opts["verbose"] = True
|
||||
elif a in ("-q", "--quiet"):
|
||||
opts["quiet"] = True
|
||||
elif a in ("-j", "--jobs") and i + 1 < len(args):
|
||||
try:
|
||||
opts["jobs"] = max(1, int(args[i + 1]))
|
||||
except ValueError:
|
||||
pass
|
||||
i += 1
|
||||
elif not a.startswith("-"):
|
||||
opts["positional"].append(a)
|
||||
i += 1
|
||||
return opts
|
||||
|
||||
def _collect_py_files(self, root: str) -> List[str]:
|
||||
root = os.path.abspath(root)
|
||||
if not os.path.isdir(root):
|
||||
return []
|
||||
result: List[str] = []
|
||||
for dirpath, dirnames, filenames in os.walk(root):
|
||||
dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS]
|
||||
for fn in filenames:
|
||||
if not fn.endswith(".py"):
|
||||
continue
|
||||
if fn in SKIP_FILES:
|
||||
continue
|
||||
result.append(os.path.join(dirpath, fn))
|
||||
result.sort()
|
||||
return result
|
||||
|
||||
def _verify_one(self, filepath: str) -> Tuple[str, bool, str]:
|
||||
try:
|
||||
with open(filepath, "r", encoding="utf-8") as f:
|
||||
src = f.read()
|
||||
except UnicodeDecodeError:
|
||||
try:
|
||||
with open(filepath, "r", encoding="gbk") as f:
|
||||
src = f.read()
|
||||
except Exception as e:
|
||||
return filepath, False, f"读取失败: {e}"
|
||||
except Exception as e:
|
||||
return filepath, False, f"读取失败: {e}"
|
||||
|
||||
try:
|
||||
ast.parse(src, filename=filepath)
|
||||
except SyntaxError as e:
|
||||
line = getattr(e, "lineno", "?")
|
||||
col = getattr(e, "offset", "?")
|
||||
msg = getattr(e, "msg", str(e))
|
||||
return filepath, False, f"SyntaxError 第{line}行第{col}列: {msg}"
|
||||
|
||||
try:
|
||||
py_compile.compile(filepath, doraise=True, quiet=2)
|
||||
except py_compile.PyCompileError as e:
|
||||
return filepath, False, f"PyCompileError: {e.msg if hasattr(e, 'msg') else e}"
|
||||
except Exception as e:
|
||||
return filepath, False, f"编译失败: {e}"
|
||||
|
||||
return filepath, True, ""
|
||||
|
||||
def _run_batch(
|
||||
self,
|
||||
files: List[str],
|
||||
title: str,
|
||||
verbose: bool = False,
|
||||
quiet: bool = False,
|
||||
jobs: int = 8,
|
||||
) -> Tuple[int, int, List[Tuple[str, str]]]:
|
||||
total = len(files)
|
||||
if total == 0:
|
||||
self._emit(self._warn(f"{title}: 没有可验证的 Python 文件"))
|
||||
return 0, 0, []
|
||||
|
||||
if not quiet:
|
||||
self._emit(self._banner(f"{title} ({total} 个文件, {jobs} 线程)"))
|
||||
self._emit("")
|
||||
|
||||
passed = 0
|
||||
failed: List[Tuple[str, str]] = []
|
||||
start = time.time()
|
||||
|
||||
results: List[Tuple[str, bool, str]] = []
|
||||
if jobs == 1:
|
||||
for f in files:
|
||||
results.append(self._verify_one(f))
|
||||
else:
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=jobs) as ex:
|
||||
futures = {ex.submit(self._verify_one, f): f for f in files}
|
||||
done = 0
|
||||
for fut in concurrent.futures.as_completed(futures):
|
||||
results.append(fut.result())
|
||||
done += 1
|
||||
if not quiet and not verbose and total > 0:
|
||||
pct = done * 100 // total
|
||||
rel = os.path.relpath(futures[fut], PROJECT_ROOT)
|
||||
self._emit(f"\r 进度: [{done}/{total}] {pct:3d}% {rel[:40]:<40}", end="")
|
||||
if not quiet and not verbose:
|
||||
self._emit("")
|
||||
|
||||
results.sort(key=lambda x: x[0])
|
||||
|
||||
if verbose and not quiet:
|
||||
self._emit("")
|
||||
|
||||
for filepath, ok, err in results:
|
||||
rel = os.path.relpath(filepath, PROJECT_ROOT)
|
||||
if ok:
|
||||
passed += 1
|
||||
if verbose and not quiet:
|
||||
self._emit(f" {self._ok('✓')} {rel}")
|
||||
else:
|
||||
failed.append((rel, err))
|
||||
if not quiet:
|
||||
self._emit(f" {self._err('✗')} {rel}")
|
||||
self._emit(f" {self._err(err)}")
|
||||
|
||||
elapsed = time.time() - start
|
||||
|
||||
self._emit("")
|
||||
self._emit("─" * 60)
|
||||
if failed:
|
||||
self._emit(f" {self._err('✗ 失败:')} {len(failed)}/{total}")
|
||||
self._emit(f" {self._ok('✓ 通过:')} {passed}/{total}")
|
||||
self._emit(f" {self._dim(f'耗时: {elapsed:.2f}s')}")
|
||||
self._emit("")
|
||||
self._emit(self._err("失败列表:"))
|
||||
for rel, err in failed:
|
||||
self._emit(f" • {self._err(rel)}")
|
||||
first_line = err.split("\n")[0][:120]
|
||||
self._emit(f" {first_line}")
|
||||
else:
|
||||
self._emit(f" {self._ok(f'✓ 全部通过:')} {passed}/{total} 个文件")
|
||||
self._emit(f" {self._dim(f'耗时: {elapsed:.2f}s')}")
|
||||
self._emit("─" * 60)
|
||||
|
||||
return passed, len(failed), failed
|
||||
|
||||
def _op_all(self, opts: dict):
|
||||
files = self._collect_py_files(PROJECT_ROOT)
|
||||
passed, failed, _ = self._run_batch(
|
||||
files,
|
||||
"全项目编译验证",
|
||||
verbose=opts["verbose"],
|
||||
quiet=opts["quiet"],
|
||||
jobs=opts["jobs"],
|
||||
)
|
||||
if failed == 0:
|
||||
return ToolResult.ok(
|
||||
f"全部通过 ({passed}/{passed})",
|
||||
tool_name="compile_check",
|
||||
)
|
||||
return ToolResult.fail(
|
||||
f"有 {failed} 个文件编译失败",
|
||||
ErrorCode.EXEC_FAILED,
|
||||
tool_name="compile_check",
|
||||
)
|
||||
|
||||
def _op_core(self, opts: dict):
|
||||
return self._scan_subdir("xcli_core", "xcli_core 编译验证", opts)
|
||||
|
||||
def _op_engines(self, opts: dict):
|
||||
return self._scan_subdir("ai_engines", "ai_engines 编译验证", opts)
|
||||
|
||||
def _op_plugins(self, opts: dict):
|
||||
return self._scan_subdir("plugins", "plugins 编译验证", opts)
|
||||
|
||||
def _scan_subdir(self, sub: str, title: str, opts: dict):
|
||||
target = os.path.join(PROJECT_ROOT, sub)
|
||||
if not os.path.isdir(target):
|
||||
return ToolResult.not_found(f"目录: {sub}", tool_name="compile_check")
|
||||
files = self._collect_py_files(target)
|
||||
passed, failed, _ = self._run_batch(
|
||||
files, title,
|
||||
verbose=opts["verbose"],
|
||||
quiet=opts["quiet"],
|
||||
jobs=opts["jobs"],
|
||||
)
|
||||
if failed == 0:
|
||||
return ToolResult.ok(
|
||||
f"{sub} 全部通过 ({passed})",
|
||||
tool_name="compile_check",
|
||||
)
|
||||
return ToolResult.fail(
|
||||
f"{sub} 有 {failed} 个文件编译失败",
|
||||
ErrorCode.EXEC_FAILED,
|
||||
tool_name="compile_check",
|
||||
)
|
||||
|
||||
def _op_dir(self, opts: dict):
|
||||
if not opts["positional"]:
|
||||
return ToolResult.missing("目录路径", tool_name="compile_check")
|
||||
target = os.path.abspath(opts["positional"][0])
|
||||
if not os.path.isdir(target):
|
||||
return ToolResult.not_found(f"目录: {target}", tool_name="compile_check")
|
||||
files = self._collect_py_files(target)
|
||||
passed, failed, _ = self._run_batch(
|
||||
files,
|
||||
f"目录验证: {os.path.relpath(target, PROJECT_ROOT)}",
|
||||
verbose=opts["verbose"],
|
||||
quiet=opts["quiet"],
|
||||
jobs=opts["jobs"],
|
||||
)
|
||||
if failed == 0:
|
||||
return ToolResult.ok(
|
||||
f"目录全部通过 ({passed})",
|
||||
tool_name="compile_check",
|
||||
)
|
||||
return ToolResult.fail(
|
||||
f"目录有 {failed} 个文件编译失败",
|
||||
ErrorCode.EXEC_FAILED,
|
||||
tool_name="compile_check",
|
||||
)
|
||||
|
||||
def _op_check(self, opts: dict):
|
||||
if not opts["positional"]:
|
||||
return ToolResult.missing("文件路径", tool_name="compile_check")
|
||||
|
||||
targets = []
|
||||
for p in opts["positional"]:
|
||||
fp = os.path.abspath(p)
|
||||
if not os.path.isfile(fp):
|
||||
return ToolResult.not_found(
|
||||
f"文件: {p}", tool_name="compile_check",
|
||||
)
|
||||
targets.append(fp)
|
||||
|
||||
if len(targets) == 1:
|
||||
self._emit(self._banner(
|
||||
f"单文件验证: {os.path.relpath(targets[0], PROJECT_ROOT)}"
|
||||
))
|
||||
filepath, ok, err = self._verify_one(targets[0])
|
||||
rel = os.path.relpath(filepath, PROJECT_ROOT)
|
||||
self._emit("")
|
||||
if ok:
|
||||
self._emit(f" {self._ok('✓ 通过')} {rel}")
|
||||
self._emit("─" * 60)
|
||||
return ToolResult.ok(f"通过: {rel}", tool_name="compile_check")
|
||||
else:
|
||||
self._emit(f" {self._err('✗ 失败')} {rel}")
|
||||
self._emit(f" {self._err(err)}")
|
||||
self._emit("─" * 60)
|
||||
return ToolResult.fail(
|
||||
f"编译失败: {rel}",
|
||||
ErrorCode.EXEC_FAILED,
|
||||
tool_name="compile_check",
|
||||
)
|
||||
|
||||
passed, failed, _ = self._run_batch(
|
||||
targets,
|
||||
f"批量验证 ({len(targets)} 个文件)",
|
||||
verbose=opts["verbose"],
|
||||
quiet=opts["quiet"],
|
||||
jobs=opts["jobs"],
|
||||
)
|
||||
if failed == 0:
|
||||
return ToolResult.ok(
|
||||
f"全部通过 ({passed})",
|
||||
tool_name="compile_check",
|
||||
)
|
||||
return ToolResult.fail(
|
||||
f"有 {failed} 个文件编译失败",
|
||||
ErrorCode.EXEC_FAILED,
|
||||
tool_name="compile_check",
|
||||
)
|
||||
|
||||
def _op_clean(self, opts: dict):
|
||||
target = PROJECT_ROOT
|
||||
if opts["positional"]:
|
||||
target = os.path.abspath(opts["positional"][0])
|
||||
if not os.path.isdir(target):
|
||||
return ToolResult.not_found(
|
||||
f"目录: {target}", tool_name="compile_check",
|
||||
)
|
||||
|
||||
self._emit(self._banner(f"清理 __pycache__: {os.path.relpath(target, PROJECT_ROOT)}"))
|
||||
removed = 0
|
||||
for dirpath, dirnames, _ in os.walk(target):
|
||||
for d in list(dirnames):
|
||||
if d == "__pycache__":
|
||||
full = os.path.join(dirpath, d)
|
||||
try:
|
||||
shutil.rmtree(full)
|
||||
removed += 1
|
||||
rel = os.path.relpath(full, PROJECT_ROOT)
|
||||
if not opts["quiet"]:
|
||||
self._emit(f" {self._ok('✓ 已删除')} {rel}")
|
||||
except Exception as e:
|
||||
rel = os.path.relpath(full, PROJECT_ROOT)
|
||||
self._emit(f" {self._err('✗ 失败')} {rel}: {e}")
|
||||
self._emit("")
|
||||
self._emit("─" * 60)
|
||||
if removed:
|
||||
self._emit(f" {self._ok(f'✓ 已清理')} {removed} 个 __pycache__")
|
||||
else:
|
||||
self._emit(f" {self._dim('没有可清理的 __pycache__')}")
|
||||
self._emit("─" * 60)
|
||||
|
||||
return ToolResult.ok(
|
||||
f"清理完成 ({removed} 个目录)",
|
||||
tool_name="compile_check",
|
||||
)
|
||||
|
||||
def _op_stats(self, opts: dict):
|
||||
self._emit(self._banner("项目 Python 文件统计"))
|
||||
|
||||
subdirs = [
|
||||
("xcli_core", "xcli_core"),
|
||||
("ai_engines", "ai_engines"),
|
||||
("plugins", "plugins"),
|
||||
]
|
||||
|
||||
total = 0
|
||||
rows: List[Tuple[str, int]] = []
|
||||
for sub, label in subdirs:
|
||||
target = os.path.join(PROJECT_ROOT, sub)
|
||||
if os.path.isdir(target):
|
||||
count = len(self._collect_py_files(target))
|
||||
rows.append((label, count))
|
||||
total += count
|
||||
|
||||
other = len(self._collect_py_files(PROJECT_ROOT)) - total
|
||||
rows.append(("其他(根目录等)", other))
|
||||
total += other
|
||||
|
||||
self._emit("")
|
||||
self._emit(f" {'目录':<20} {'文件数':>8}")
|
||||
self._emit(" " + "─" * 30)
|
||||
for label, count in rows:
|
||||
bar = "█" * min(count, 40) if count else ""
|
||||
color = Fore.GREEN if count else Fore.CYAN
|
||||
self._emit(f" {label:<20} {count:>8} {color}{bar}{Style.RESET_ALL}")
|
||||
self._emit(" " + "─" * 30)
|
||||
self._emit(f" {'合计':<20} {total:>8}")
|
||||
self._emit("")
|
||||
self._emit("─" * 60)
|
||||
|
||||
return ToolResult.ok(
|
||||
f"统计完成 (共 {total} 个文件)",
|
||||
tool_name="compile_check",
|
||||
)
|
||||
Reference in new issue
Block a user