diff --git a/README.md b/README.md index 0c75da5..396bb4c 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ LLM chat interfaces. Paste LLM-generated tool calls (in XML format), review and audit dangerous operations, and manage sessions with full history tracking -- all running locally on your machine. -> **Version**: 0.4.0 | **Python**: >=3.14 +> **Version**: 0.4.1 | **Python**: >=3.14 --- diff --git a/README_ZH.md b/README_ZH.md index ef0256f..2a4dc7e 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -8,7 +8,7 @@ ManualAid 提供了一个基于 Textual 的 TUI 控制台,在剪贴板和 LLM 聊天界面之间架起桥梁. 粘贴 LLM 生成的工具调用(XML 格式),审查和审计危险操作,并通过完整的历史追踪管理会话 -- 一切都在本地运行. -> **版本**: 0.4.0 | **Python**: >=3.14 +> **版本**: 0.4.1 | **Python**: >=3.14 --- diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index e05f132..b9b7b89 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -8,6 +8,29 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.4.1] - 2026-05-04 + +### Added + +- **Single File Search Support**: Extended search tools (`exact_search`, + `regex_search`) to handle single file paths directly. The `files_to_search` + initialization now includes `is_file()` branching with adjusted + `relative_path` calculation for non-directory scenarios + ([#110](https://github.com/SunYanbox/ManualAid/issues/110)). + +### Fixed + +- **Audit Tab MarkupError Crash**: Fixed Rich Markup parsing errors in the audit + tab caused by unescaped text. Set `markup=False` on `Static` widgets and + applied `rich.markup.escape()` to dynamically generated log results, + preventing crashes when rendering approve/reject statuses containing special + characters ([#124](https://github.com/SunYanbox/ManualAid/issues/124)). +- **Single File Search Failure**: Fixed a bug where `exact_search` and + `regex_search` tools failed to read content when the search path pointed to a + single file instead of a directory, due to the recursive glob (`rglob`) not + handling file paths + ([#110](https://github.com/SunYanbox/ManualAid/issues/110)). + ## [0.4.0] - 2026-05-03 ### Added @@ -153,5 +176,7 @@ and this project adheres to _Initial release features and history._ +[0.4.1]: https://github.com/SunYanbox/ManualAid/releases/tag/v0.4.1 +[0.4.0]: https://github.com/SunYanbox/ManualAid/releases/tag/v0.4.0 [0.3.0]: https://github.com/SunYanbox/ManualAid/releases/tag/v0.3.0 [0.2.0]: https://github.com/SunYanbox/ManualAid/releases/tag/v0.2.0 diff --git a/docs/CHANGELOG_ZH.md b/docs/CHANGELOG_ZH.md index 3af5591..d8fd008 100644 --- a/docs/CHANGELOG_ZH.md +++ b/docs/CHANGELOG_ZH.md @@ -8,6 +8,25 @@ [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/). 并采用 [语义化版本](https://semver.org/lang/Chinese/). +## [0.4.1] - 2026-05-04 + +### 新增 + +- **单文件路径搜索支持**: 扩展 `exact_search`、`regex_search` + 工具以直接处理单文件路径. `files_to_search` 初始化阶段增加了 `is_file()` + 分支判断,并调整了 `relative_path` + 计算逻辑以适配非目录场景 ([#110](https://github.com/SunYanbox/ManualAid/issues/110)). + +### 修复 + +- **审核标签页 MarkupError 崩溃**: 修复了审核标签页中因未转义文本导致的 Rich + Markup 解析错误. 在 `Static` 组件上设置 `markup=False` + 并对动态生成的日志结果调用 + `rich.markup.escape()`,防止渲染包含特殊字符的批准/拒绝状态时崩溃 ([#124](https://github.com/SunYanbox/ManualAid/issues/124)). +- **单文件搜索失效**: 修复了当搜索路径指向单个文件时,`exact_search` 和 + `regex_search` 工具因递归查找 (`rglob`) 无法处理文件路径而无法读取内容的 Bug + ([#110](https://github.com/SunYanbox/ManualAid/issues/110)). + ## [0.4.0] - 2026-05-03 ### 新增 @@ -125,5 +144,7 @@ _初始发布的功能和历史记录._ +[0.4.1]: https://github.com/SunYanbox/ManualAid/releases/tag/v0.4.1 +[0.4.0]: https://github.com/SunYanbox/ManualAid/releases/tag/v0.4.0 [0.3.0]: https://github.com/SunYanbox/ManualAid/releases/tag/v0.3.0 [0.2.0]: https://github.com/SunYanbox/ManualAid/releases/tag/v0.2.0 diff --git a/pyproject.toml b/pyproject.toml index 35863e1..5d32ecb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ManualAid" -version = "0.4.0" +version = "0.4.1" description = "" requires-python = ">=3.14" dependencies = [ diff --git a/src/console/ui/widgets/audit_tab.py b/src/console/ui/widgets/audit_tab.py index b355e5b..3233a89 100644 --- a/src/console/ui/widgets/audit_tab.py +++ b/src/console/ui/widgets/audit_tab.py @@ -5,6 +5,7 @@ from collections import defaultdict from typing import ClassVar +from rich.markup import escape from textual.containers import Horizontal, Vertical from textual.widgets import Button, Collapsible, Label, Static @@ -136,7 +137,7 @@ async def _refresh(self) -> None: snap_id = snap[0] diff_content = snap[4] or "(空 diff)" - diff_container = Vertical(Static(diff_content), classes="audit-diff") + diff_container = Vertical(Static(diff_content, markup=False), classes="audit-diff") btn_row = Horizontal( Button("批准", variant="primary", id=f"approve-{snap_id}", classes="audit-approve"), Button("拒绝", variant="error", id=f"reject-{snap_id}", classes="audit-reject"), @@ -186,7 +187,8 @@ async def on_button_pressed(self, event: Button.Pressed) -> None: log = self.query_one("#audit-result-log", Vertical) color = "green" if "已批准" in result or "已拒绝" in result else "red" - await log.mount(Static(f"[{color}]{result}[/{color}]")) + escaped = escape(result) + await log.mount(Static(f"[{color}]{escaped}[/{color}]")) except Exception: pass diff --git a/src/constants/__init__.py b/src/constants/__init__.py index 6a9beea..3d26edf 100644 --- a/src/constants/__init__.py +++ b/src/constants/__init__.py @@ -1 +1 @@ -__version__ = "0.4.0" +__version__ = "0.4.1" diff --git a/src/workspace/tools/exact_search_tool.py b/src/workspace/tools/exact_search_tool.py index c035399..b23eaa6 100644 --- a/src/workspace/tools/exact_search_tool.py +++ b/src/workspace/tools/exact_search_tool.py @@ -118,39 +118,43 @@ def exact_search( results = [] file_count = 0 - # 遍历所有文件(递归) - for file_path in search_path.rglob("*"): - if file_path.is_file(): - # 检查是否达到限制 - if len(results) >= limit: - break + # 确定要搜索的文件列表(支持单文件或目录) + files_to_search = [search_path] if search_path.is_file() else list(search_path.rglob("*")) + + # 遍历所有文件 + for file_path in files_to_search: + if not file_path.is_file(): + continue + # 检查是否达到限制 + if len(results) >= limit: + break - # 检查是否应该忽略 - should_ignore = False - relative_path = file_path.relative_to(search_path) + # 检查是否应该忽略 + should_ignore = False + relative_path = file_path.relative_to(search_path) if search_path.is_dir() else file_path - for ignore_pattern in ignore_patterns: - if ignore_pattern.search(str(relative_path)): - should_ignore = True - break + for ignore_pattern in ignore_patterns: + if ignore_pattern.search(str(relative_path)): + should_ignore = True + break - if should_ignore: - continue + if should_ignore: + continue - try: - # 读取文件内容 - with open(file_path, encoding="utf-8") as f: - lines = f.readlines() + try: + # 读取文件内容 + with open(file_path, encoding="utf-8") as f: + lines = f.readlines() - # 搜索匹配行 - file_matches = _search_exact_in_file(lines, search_string, case_sensitive, whole_word) + # 搜索匹配行 + file_matches = _search_exact_in_file(lines, search_string, case_sensitive, whole_word) - if file_matches: - results.append({"file": str(file_path), "matches": file_matches}) - file_count += 1 + if file_matches: + results.append({"file": str(file_path), "matches": file_matches}) + file_count += 1 - except OSError, UnicodeDecodeError, PermissionError: - continue # 跳过无法读取的文件 + except OSError, UnicodeDecodeError, PermissionError: + continue # 跳过无法读取的文件 # 格式化输出 return _format_exact_results(results, pattern, limit, file_count, case_sensitive, whole_word) diff --git a/src/workspace/tools/regex_search_tool.py b/src/workspace/tools/regex_search_tool.py index 98e8ed2..59114ad 100644 --- a/src/workspace/tools/regex_search_tool.py +++ b/src/workspace/tools/regex_search_tool.py @@ -146,15 +146,20 @@ def regex_search( results = [] file_count = 0 + # 确定要搜索的文件列表(支持单文件或目录) + files_to_search = [search_path] if search_path.is_file() else list(search_path.rglob(file_pattern)) + # 遍历文件 - for file_path in search_path.rglob(file_pattern): + for file_path in files_to_search: + if not file_path.is_file(): + continue # 检查是否达到限制 if len(results) >= limit: break # 检查是否应该忽略该文件或文件夹 should_ignore = False - relative_path = file_path.relative_to(search_path) + relative_path = file_path.relative_to(search_path) if search_path.is_dir() else file_path for ignore_pattern in ignore_patterns: # 检查是否匹配忽略模式 diff --git a/src/workspace/workspace.py b/src/workspace/workspace.py index cf3d8ed..538a280 100644 --- a/src/workspace/workspace.py +++ b/src/workspace/workspace.py @@ -72,11 +72,6 @@ def search_content( try: path = self.path_validator.validate(folder_path) - if not path.is_dir(): - return ToolErrorResponse( - self.search_content.__name__, ValueError(f"{path} is not a directory") - ).to_str() - # 初始化排除目录集合 exclude_set = set(exclude_dirs or [".git", "__pycache__", "node_modules", ".venv", "venv", "dist", "build"]) @@ -87,18 +82,21 @@ def search_content( except re.error as e: return f"错误:正则表达式无效 - {e}" - # 收集所有要搜索的文件 + # 收集所有要搜索的文件(支持单文件或目录) files_to_search = [] - for file_path in path.rglob(file_pattern): - if file_path.is_file(): - # 检查是否在排除目录中 - should_exclude = False - for parent in file_path.parents: - if parent.name in exclude_set: - should_exclude = True - break - if not should_exclude: - files_to_search.append(file_path) + if path.is_file(): + files_to_search = [path] + else: + for file_path in path.rglob(file_pattern): + if file_path.is_file(): + # 检查是否在排除目录中 + should_exclude = False + for parent in file_path.parents: + if parent.name in exclude_set: + should_exclude = True + break + if not should_exclude: + files_to_search.append(file_path) if not files_to_search: return f"在 {folder_path} 中没有找到匹配 {file_pattern} 的文件"