From 417fda5479ca5a841279e42894d3a24355e23d54 Mon Sep 17 00:00:00 2001 From: qing <1776177662@qq.com> Date: Mon, 3 Aug 2026 22:52:12 +0800 Subject: [PATCH] =?UTF-8?q?refactor(aibox):=20=E6=96=87=E4=BB=B6/=E7=BD=91?= =?UTF-8?q?=E7=BB=9C=E6=93=8D=E4=BD=9C=E5=B7=A5=E5=85=B7=E6=94=B9=E4=B8=BA?= =?UTF-8?q?=E8=BF=94=E5=9B=9E=E5=91=BD=E4=BB=A4=E8=A1=8C=E6=8C=87=E5=BC=95?= =?UTF-8?q?=E5=B9=B6=E7=A7=BB=E9=99=A4beautifulsoup4=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- aibox/README.md | 10 +- aibox/aibox.py | 522 +++++++----------------- aibox/requirements.txt | 1 - docs/aibox_file_network_cli_refactor.md | 11 + 4 files changed, 159 insertions(+), 385 deletions(-) diff --git a/aibox/README.md b/aibox/README.md index 2283157..6a71a1b 100644 --- a/aibox/README.md +++ b/aibox/README.md @@ -127,10 +127,10 @@ AI 智能体可以调用以下工具来完成任务(由 `Tool` 基类派生, | 工具名称 | 功能描述 | |---------|---------| -| `read_file` | 读取文件内容(支持分页、head/tail) | -| `write_file` | 写入或追加文件 | -| `list_files` | 列出目录内容 | -| `web` | 访问网页、获取标题/文本/链接 | +| `read_file` | 读取文件内容(返回 markdown 指引,改用 `head`/`tail`/`sed`/`wc` 等命令行读取) | +| `write_file` | 写入或追加文件(返回 markdown 指引,改用 `mkdir -p` + `cat` 重定向写入) | +| `list_files` | 列出目录内容(返回 markdown 指引,改用 `ls`/`du`/`find` 命令行列出) | +| `web` | 访问网页、获取标题/文本/链接(返回 markdown 指引,改用 `curl` 命令行访问) | | `memory` | 保存、搜索(支持分页)、删除记忆 | | `memo` | 创建、查看、完成、搜索任务 | | `run_command` | 执行系统命令(实时输出,支持超时控制) | @@ -639,7 +639,7 @@ class MyTool(Tool): ``` > **注意**:`execute()` 必须返回字符串。工具结果会经过多层安全防护: -> 1. **工具层源头限制**:`read_file` 按需读取不全载(最多500行,单行≤10KB),`run_command` 输出≤1MB(超出立即终止进程) +> 1. **工具层源头限制**:文件/网页工具返回 markdown 指引,实际 I/O 由 `run_command` 完成(`run_command` 输出≤1MB,超出立即终止进程;`read_file` 指引命令限制 500 行、单行≤10KB) > 2. **`_sanitize_tool_result()`**:None/非字符串转为安全字符串,过滤控制字符 > 3. **最终兜底截断**:超过 `tools.max_result_length`(默认1MB字符)时截断并提示,避免破坏 API 请求格式 diff --git a/aibox/aibox.py b/aibox/aibox.py index c8b5eac..55fe75e 100644 --- a/aibox/aibox.py +++ b/aibox/aibox.py @@ -27,7 +27,6 @@ import websockets from abc import ABC, abstractmethod from datetime import datetime, timedelta from typing import List, Dict, Optional, Any, Tuple, Callable, Union, Set -from urllib.parse import urljoin, urlparse import argparse from pathlib import Path import signal @@ -35,7 +34,6 @@ import logging from logging.handlers import RotatingFileHandler import requests -from bs4 import BeautifulSoup # ==================== 语言检测和多语言支持 ==================== @@ -712,10 +710,16 @@ save_memory_and_end_conversation( - 用户明确指定文件路径时,按照用户要求的路径操作 - 读取系统文件、项目文件时使用实际路径,不限制 -## 可用工具 -- **read_file**: 读取文件内容 -- **write_file**: 写入文件 -- **list_files**: 列出目录 +## 重要:文件操作改用命令行 +`read_file` / `write_file` / `list_files` 工具会返回提示你使用 `run_command` 的 markdown 指引,**请按指引执行命令完成实际操作**,不要直接返回指引内容。 + +优先使用以下命令行工具: +- **读取文件**:`head -n N 文件`、`tail -n N 文件`、`sed -n 'A,Bp' 文件` +- **统计行数**:`wc -l 文件` +- **查看编码**:`file -b --mime-encoding 文件` +- **写入文件**:`mkdir -p 目录 && cat > 文件 <<'EOF' ... EOF`(覆盖)或 `cat >> 文件`(追加) +- **列出目录**:`ls -l 目录`、`ls -la 目录`(含隐藏文件) +- **目录大小**:`du -sh 目录` ## 大文件处理规则 当遇到以下情况时,**必须分页读取**,不要一次性读取整个文件: @@ -723,42 +727,41 @@ save_memory_and_end_conversation( 2. 文件行数超过 2000 行 对于大文件,应该: -- 先用 head=true 读取前50行了解文件结构 -- 使用 start_line/end_line 参数指定行范围 -- 使用 tail=true 读取文件末尾 -- 对于二进制文件或特殊格式文件,使用 run_command 配合 head/strings 等命令查看 +- 先用 `head -n 50` 读取前50行了解文件结构 +- 使用 `sed -n 'A,Bp'` 指定行范围 +- 使用 `tail -n N` 读取文件末尾 +- 对于二进制文件或特殊格式文件,使用 `head`/`strings` 等命令查看 - 建议用户提供更具体的查询条件以缩小范围 -## 示例 -``` -read_file filepath="/path/to/file.txt" head=true max_lines=50 -read_file filepath="/path/to/file.txt" start_line=100 end_line=200 -read_file filepath="/path/to/file.txt" tail=true max_lines=50 -``` +## 安全限制 +- 单次最多读取 500 行,单行最长 10000 字符(超长用 `cut -c 1-10000` 截断) +- 文件大小上限 10MB,路径参数需用引号包裹 """ }, { 'filename': '4_web_access.md', 'content': """# 网页访问能力 -## 可用工具 -- **web**: 访问网页获取内容 +## 重要:网页访问改用 curl +`web` 工具会返回提示你使用 `run_command` 执行 `curl` 命令的 markdown 指引,**请按指引执行命令完成实际访问**,不要直接返回指引内容。 -## 使用方法 -- web action="get" url="https://example.com" # 获取网页信息 -- web action="text" url="https://example.com" # 获取纯文本 -- web action="title" url="https://example.com" # 获取标题 -- web action="links" url="https://example.com" # 获取所有链接 +## 常用 curl 命令 +- **获取网页**:`curl -sL --max-time 10 URL` +- **查看状态码**:`curl -sL -o /dev/null -w '%{http_code}' URL` +- **提取纯文本**:`curl -sL URL | sed -r 's/<[^>]+>//g'`(有 lynx 时用 `lynx -dump -stdin` 更准) +- **提取标题**:`curl -sL URL | grep -oP '\\K[^<]+'` +- **提取链接**:`curl -sL URL | grep -oP 'href="\\K[^"]+'` +- **搜索关键词**:`curl -sL URL | grep -c 关键词` +- **下载文件**:`curl -sL URL -o 文件名` ## 推荐搜索引擎 - 百度:https://www.baidu.com - 必应:https://cn.bing.com - 搜狗:https://www.sogou.com -## 示例 -``` -web action="get" url="https://www.baidu.com/s?wd=人工智能" -``` +## 注意 +- 下载大小默认上限 100MB,可用 `| head -c 100M` 限制 +- 如需代理,先调用 `set_proxy` 工具配置,`curl` 会自动附带 `--socks5` """ }, { @@ -5804,100 +5807,11 @@ class MemoryTool(Tool): # ==================== 网页工具(修改版,支持流式输出) ==================== -class WebPage: - """网页对象""" - - def __init__(self, url: str, response: requests.Response = None): - self.url = url - self.status_code = None - self.headers = {} - self.html = "" - self.soup = None - self.error = None - self.fetch_time = None - - if response: - self.status_code = response.status_code - self.headers = dict(response.headers) - self.html = response.text - self.fetch_time = datetime.now() - try: - self.soup = BeautifulSoup(self.html, 'html.parser') - except: - pass - - @property - def title(self) -> Optional[str]: - if self.soup and self.soup.find('title'): - return self.soup.find('title').get_text(strip=True) - return None - - @property - def text(self) -> str: - if self.soup: - return self.soup.get_text(separator='\n', strip=True) - return "" - - @property - def links(self) -> List[str]: - links = [] - if self.soup: - for a in self.soup.find_all('a', href=True): - href = a['href'].strip() - if href and not href.startswith('#') and not href.startswith('javascript:'): - links.append(urljoin(self.url, href)) - return list(set(links)) - - -class SimpleWebClient: - """简单网页访问客户端""" - - def __init__(self, timeout: int = None, delay: float = None, user_agent: str = None): - self.timeout = timeout or constants.WEB_TIMEOUT - self.delay = delay if delay is not None else constants.WEB_DELAY - - self.session = requests.Session() - self.session.headers.update({ - 'User-Agent': user_agent or constants.WEB_USER_AGENT - }) - - self.last_request_time = 0 - - def _wait_if_needed(self): - if self.delay > 0: - elapsed = time.time() - self.last_request_time - if elapsed < self.delay: - time.sleep(self.delay - elapsed) - - def get(self, url: str, **kwargs) -> WebPage: - self._wait_if_needed() - # 每次都从 ConfigManager 获取最新代理 - proxies = ConfigManager().get_proxy() - try: - response = self.session.get( - url, - timeout=kwargs.get('timeout', self.timeout), - proxies=proxies, # 传入最新代理 - **kwargs - ) - response.raise_for_status() - - page = WebPage(url, response) - self.last_request_time = time.time() - return page - - except Exception as e: - page = WebPage(url) - page.error = str(e) - return page - - class SimpleWebCrawlerTool(Tool): - """简化版网页爬虫工具(支持流式输出)""" + """简化版网页爬虫工具(返回 markdown 提示词,指导改用 curl 命令行)""" def __init__(self, logger=None): super().__init__(logger) - self.client = SimpleWebClient() def get_name(self) -> str: return "web" @@ -5920,36 +5834,57 @@ class SimpleWebCrawlerTool(Tool): "required": ["action", "url"] } + def _proxy_curl_args(self) -> str: + """根据当前代理配置构造 curl --socks5 参数""" + proxies = ConfigManager().get_proxy() + if not proxies: + return "" + proxy_url = proxies.get("https") or proxies.get("http") or "" + host_port = proxy_url.split("://", 1)[-1] + if "@" in host_port: + host_port = host_port.split("@", 1)[-1] + return f"--socks5 {shlex.quote(host_port)}" + def execute(self, action: str, url: str, **kwargs) -> str: - try: - if action == "get": - page = self.client.get(url) - return self._format_page_info(page) - elif action == "text": - text = self.client.get(url).text - return f"📄 网页文本内容:\n\n{text[:1000]}{'...' if len(text)>1000 else ''}" - elif action == "title": - title = self.client.get(url).title - return f"📌 网页标题: {title}" if title else "❌ 未找到标题" - elif action == "links": - links = self.client.get(url).links - result = [f"🔗 找到 {len(links)} 个链接:"] - for i, link in enumerate(links[:20]): - result.append(f" {i+1}. {link}") - return "\n".join(result) - elif action == "search": - keyword = kwargs.get("keyword") - if not keyword: - return "❌ 需要提供 keyword 参数" - page = self.client.get(url) - if keyword.lower() in page.text.lower(): - return f"🔎 在网页中找到关键词 '{keyword}'" - else: - return f"❌ 未找到关键词 '{keyword}'" - else: - return f"❌ 未知操作: {action}" - except Exception as e: - return f"❌ 执行错误: {str(e)}" + if action not in ("get", "text", "title", "links", "search"): + return f"❌ 未知操作: {action}" + + quoted_url = shlex.quote(url) + timeout = constants.WEB_TIMEOUT + user_agent = shlex.quote(constants.WEB_USER_AGENT) + proxy = self._proxy_curl_args() + base = f"curl -sL --max-time {timeout} -A {user_agent} {proxy}".strip() + + if action == "get": + cmd = f"{base} -w '\\nHTTP_CODE:%{{http_code}}' {quoted_url}" + extra = "如需查看响应头:`curl -sIL ...`;如需保存到文件:`curl -sL ... -o 文件名`" + elif action == "text": + cmd = f"{base} {quoted_url} | sed -r 's/<[^>]+>//g'" + extra = "若系统装有 lynx,可用 `lynx -dump -stdin` 提取纯文本更准确" + elif action == "title": + cmd = f"{base} {quoted_url} | grep -oP '<title>\\K[^<]+'" + extra = "无输出则表示未找到标题" + elif action == "links": + cmd = f"{base} {quoted_url} | grep -oP 'href=\"\\K[^\"]+'" + extra = "输出所有链接 URL,可配合 `head` 限制数量" + else: # search + keyword = kwargs.get("keyword") + if not keyword: + return "❌ 需要提供 keyword 参数" + cmd = f"{base} {quoted_url} | grep -c {shlex.quote(keyword)}" + extra = f"返回匹配行数,大于 0 即表示找到关键词 '{keyword}'" + + # 返回 markdown 提示词,指导智能体用 curl 完成网页访问 + prompt = f"""请使用 `run_command` 工具执行以下命令来完成网页操作,不要直接返回网页内容: + +```bash +{cmd} +``` + +{extra} +- 下载大小限制 {constants.WEB_MAX_DOWNLOAD_SIZE} 字节,可在命令后追加 `| head -c {constants.WEB_MAX_DOWNLOAD_SIZE}` 限制 +- 当前已按配置自动附带代理:{proxy or '无(如需代理请先调用 set_proxy 工具)'}""" + return prompt def execute_streaming(self, output_queue: queue.Queue, **kwargs): """流式执行网页操作""" @@ -5966,23 +5901,6 @@ class SimpleWebCrawlerTool(Tool): # 发送结果 output_queue.put(("line", result + "\n")) output_queue.put(("complete", result)) - - def _format_page_info(self, page: WebPage) -> str: - if page.error: - return f"❌ 获取失败: {page.error}" - - lines = [ - f"🌐 URL: {page.url}", - f"📊 状态码: {page.status_code}", - f"📌 标题: {page.title or '无标题'}", - f"🔗 链接数: {len(page.links)}", - ] - - if page.text: - preview = page.text[:200].replace('\n', ' ') - lines.append(f"📄 预览: {preview}...") - - return "\n".join(lines) # ==================== 文件操作工具(修改版,支持流式输出) ==================== @@ -6041,88 +5959,6 @@ class ReadFileTool(FileToolBase): "required": ["filepath"] } - def _calculate_line_range(self, line_count: int, start_line: int, end_line: int, - max_lines: int, head: bool, tail: bool) -> Tuple[int, int, str]: - if head: - start = 0 - end = min(max_lines, line_count) - desc = f"开头 {end} 行" - elif tail: - start = max(0, line_count - max_lines) - end = line_count - desc = f"结尾 {line_count - start} 行" - elif end_line > 0: - start = max(0, start_line - 1) - end = min(end_line, line_count) - desc = f"第 {start_line} 行到第 {end_line} 行" - else: - start = max(0, start_line - 1) - if max_lines > 0: - end = min(start + max_lines, line_count) - desc = f"从第 {start_line} 行开始,最多 {max_lines} 行" - else: - end = line_count - desc = f"从第 {start_line} 行到文件末尾" - - return start, end, desc - - @staticmethod - def _count_lines(filepath: str, encoding: str) -> int: - """高效统计文件行数,不存储内容""" - line_count = 0 - with open(filepath, 'r', encoding=encoding) as f: - for _ in f: - line_count += 1 - return line_count - - @staticmethod - def _read_lines_range(filepath: str, encoding: str, start: int, end: int) -> List[str]: - """读取指定行范围 [start, end)(0-indexed),只读取需要的行""" - lines = [] - with open(filepath, 'r', encoding=encoding) as f: - for i, line in enumerate(f): - if i >= end: - break - if i >= start: - lines.append(line) - return lines - - @staticmethod - def _read_tail_lines(filepath: str, encoding: str, num_lines: int) -> Tuple[List[str], int]: - """高效读取文件末尾N行,无需遍历整个文件。返回 (lines, total_line_count)""" - # 对于小文件直接读全部 - file_size = os.path.getsize(filepath) - if file_size < 1024 * 1024: # < 1MB 直接读 - with open(filepath, 'r', encoding=encoding) as f: - all_lines = f.readlines() - return all_lines[-num_lines:] if num_lines > 0 else [], len(all_lines) - - # 大文件:从后向前找换行符 - with open(filepath, 'rb') as f: - block_size = 8192 - blocks = [] - lines_found = 0 - position = file_size - - while position > 0 and lines_found <= num_lines: - read_size = min(block_size, position) - position -= read_size - f.seek(position) - block = f.read(read_size) - blocks.append(block) - lines_found += block.count(b'\n') - - # 拼接并解码 - data = b''.join(reversed(blocks)) - text = data.decode(encoding, errors='replace') - all_lines = text.splitlines() - - # 统计总行数 - f.seek(0) - total_lines = sum(1 for _ in f) - - return all_lines[-num_lines:], total_lines - def execute(self, filepath: str, encoding: str = "utf-8", start_line: int = 1, end_line: int = 0, max_lines: int = 100, head: bool = False, @@ -6136,65 +5972,33 @@ class ReadFileTool(FileToolBase): # 限制max_lines上限,防止请求超大范围 max_lines = min(max_lines, 500) + quoted = shlex.quote(abs_path) - try: - file_size = os.path.getsize(abs_path) - modified_time = datetime.fromtimestamp(os.path.getmtime(abs_path)) - - # 高效读取:先统计行数 - line_count = self._count_lines(abs_path, encoding) - - start, end, range_desc = self._calculate_line_range( - line_count, start_line, end_line, max_lines, head, tail - ) - - if end <= start: - return f"❌ 无效的行范围" - - lines_to_read = end - start - - # 对于tail模式,用高效的尾部读取 - if tail and line_count > 200: - display_lines_raw, _ = self._read_tail_lines(abs_path, encoding, lines_to_read) - else: - display_lines_raw = self._read_lines_range(abs_path, encoding, start, end) - - # 限制单行大小(防止单行超大导致内存问题) - MAX_LINE_LENGTH = 10000 - content_lines = [] - truncated_lines = 0 - for i, line in enumerate(display_lines_raw): - line_num = start + i + 1 - content = line.rstrip('\n\r') - if len(content) > MAX_LINE_LENGTH: - content = content[:MAX_LINE_LENGTH] + f"... (行超长,已截断,原始长度 {len(content)})" - truncated_lines += 1 - content_lines.append(f" {line_num:4d} | {content}") - - data = { - "路径": abs_path, - "大小": f"{file_size} 字节 ({file_size/1024:.1f}KB)", - "最后修改": modified_time.strftime(constants.DATETIME_FORMAT), - "总行数": line_count, - "显示范围": f"{range_desc} (行 {start+1}-{end})" - } - - if content_lines: - data["内容"] = "\n" + "\n".join(content_lines) - - if end < line_count: - data["提示"] = f"还有 {line_count - end} 行未显示" - if truncated_lines > 0: - data["提示"] += f",{truncated_lines} 行因超长被截断" - - return self.format_result(True, "文件读取成功", data) - - except UnicodeDecodeError: - return f"❌ 文件编码错误:无法使用 {encoding} 编码读取文件" - except PermissionError: - return f"❌ 权限错误:无法读取文件 {abs_path}" - except Exception as e: - return f"❌ 文件读取错误: {str(e)}" + # 构造等价的命令行读取命令 + if head: + cmd = f"head -n {max_lines} {quoted}" + elif tail: + cmd = f"tail -n {max_lines} {quoted}" + elif end_line > 0: + cmd = f"sed -n '{start_line},{end_line}p' {quoted}" + elif max_lines > 0: + end = start_line + max_lines - 1 + cmd = f"sed -n '{start_line},{end}p' {quoted}" + else: + cmd = f"sed -n '{start_line},$p' {quoted}" + + # 返回 markdown 提示词,指导智能体用命令行工具读取文件 + prompt = f"""请使用 `run_command` 工具执行以下命令来读取文件内容,不要直接返回文件内容: + +```bash +{cmd} +``` + +- 若需统计总行数:`wc -l {quoted}` +- 若需查看文件编码:`file -b --mime-encoding {quoted}` +- 若单行超长,可在命令后追加 `| cut -c 1-10000` 截断到 10000 字符 +- 文件大小上限 {constants.MAX_FILE_SIZE} 字节,单次最多读取 500 行""" + return prompt def execute_streaming(self, output_queue: queue.Queue, **kwargs): """流式执行文件读取""" @@ -6242,36 +6046,27 @@ class WriteFileTool(FileToolBase): if not is_safe: return error - try: - directory = os.path.dirname(abs_path) - if directory and not os.path.exists(directory): - os.makedirs(directory, exist_ok=True) - - file_exists = os.path.exists(abs_path) - old_size = os.path.getsize(abs_path) if file_exists else 0 - - with open(abs_path, mode, encoding=encoding) as f: - f.write(content) - - new_size = len(content.encode(encoding)) - action = "覆盖写入" if mode == "w" else "追加写入" - - data = { - "路径": abs_path, - "大小": f"{new_size} 字节", - } - - if file_exists: - data["变化"] = f"{old_size} -> {new_size} 字节" - else: - data["状态"] = "新文件已创建" - - return self.format_result(True, f"文件{action}成功", data) - - except PermissionError: - return f"❌ 权限错误:无法写入文件" - except Exception as e: - return f"❌ 文件写入错误: {str(e)}" + quoted = shlex.quote(abs_path) + directory = os.path.dirname(abs_path) + dir_cmd = f"mkdir -p {shlex.quote(directory)}" if directory else "" + + # 内容写入使用 heredoc,避免引号/特殊字符转义问题 + redirect = ">" if mode == "w" else ">>" + write_cmd = f"cat {redirect} {quoted} <<'AIBOX_EOF'\n{content}\nAIBOX_EOF" + if dir_cmd: + write_cmd = f"{dir_cmd} && {write_cmd}" + + # 返回 markdown 提示词,指导智能体用命令行工具写入文件 + prompt = f"""请使用 `run_command` 工具执行以下命令来写入文件,不要在此工具中直接写入: + +```bash +{write_cmd} +``` + +- 如遇 heredoc 内容含特殊字符导致解析失败,可用 `printf '%s' '内容' > {quoted}` 覆盖写入 +- 若需检查文件大小:`wc -c {quoted}` +- 文件大小上限 {constants.MAX_FILE_SIZE} 字节""" + return prompt def execute_streaming(self, output_queue: queue.Queue, **kwargs): """流式执行文件写入""" @@ -6318,52 +6113,21 @@ class ListFilesTool(FileToolBase): if not os.path.isdir(abs_path): return f"❌ 路径不是目录: {abs_path}" - try: - items = [] - total_size = 0 - file_count = 0 - dir_count = 0 - - for item in sorted(os.listdir(abs_path)): - if not show_hidden and item.startswith('.'): - continue - - item_path = os.path.join(abs_path, item) - - if os.path.isdir(item_path): - items.append(f"📁 {item}/") - dir_count += 1 - else: - size = os.path.getsize(item_path) - total_size += size - file_count += 1 - - if size < 1024: - size_str = f"{size}B" - elif size < 1024*1024: - size_str = f"{size/1024:.1f}KB" - else: - size_str = f"{size/1024/1024:.1f}MB" - - items.append(f"📄 {item} ({size_str})") - - data = { - "目录": abs_path, - "统计": f"{file_count} 个文件, {dir_count} 个目录", - "总大小": f"{total_size/1024:.1f}KB" if file_count > 0 else "0KB" - } - - if items: - data["内容"] = "\n" + "\n".join(items[:50]) - if len(items) > 50: - data["提示"] = f"还有 {len(items)-50} 个项目未显示" - else: - data["内容"] = "\n📭 目录为空" - - return self.format_result(True, "目录列表成功", data) - - except Exception as e: - return f"❌ 列出目录错误: {str(e)}" + quoted = shlex.quote(abs_path) + flag = "-la" if show_hidden else "-l" + + # 返回 markdown 提示词,指导智能体用命令行工具列目录 + prompt = f"""请使用 `run_command` 工具执行以下命令来列出目录内容,不要直接在此工具中列出: + +```bash +ls {flag} {quoted} +``` + +- 若需区分目录/文件并显示大小:`ls -lhF {quoted}` +- 若需统计目录总大小:`du -sh {quoted}` +- 若需递归列出:`find {quoted} -maxdepth 2` +- 若需了解文件类型:`file {quoted}/*`""" + return prompt def execute_streaming(self, output_queue: queue.Queue, **kwargs): """流式执行列出目录""" diff --git a/aibox/requirements.txt b/aibox/requirements.txt index 255a394..47836fe 100644 --- a/aibox/requirements.txt +++ b/aibox/requirements.txt @@ -1,7 +1,6 @@ # backend websockets requests -beautifulsoup4 # frontend websockets>=11.0 diff --git a/docs/aibox_file_network_cli_refactor.md b/docs/aibox_file_network_cli_refactor.md index 0e89d1e..3b31477 100644 --- a/docs/aibox_file_network_cli_refactor.md +++ b/docs/aibox_file_network_cli_refactor.md @@ -18,6 +18,8 @@ ## 阶段一:文件操作命令行化 +> **实现说明(已落地)**:阶段一采用「工具返回 markdown 提示词」的方式而非工具内部调子进程。`read_file` / `write_file` / `list_files` 三个工具的 `execute()` 不再执行 Python 文件 I/O,而是返回一段 markdown 提示词,指导智能体改用 `run_command` 工具执行对应的命令行命令(`head`/`tail`/`sed`/`wc`/`file`/`cut`/`cat`/`ls`/`du`/`find`)。工具接口(名称与参数)保持不变,安全限制(500 行、10KB/行、10MB 文件、路径 `shlex.quote` 转义)在提示词中注明。 + ### 1.1 ReadFileTool 重构 用以下命令行工具替代 Python 文件 I/O: @@ -71,6 +73,8 @@ ## 阶段二:网络操作命令行化 +> **实现说明(已落地)**:阶段二沿用阶段一的方式,`web` 工具(`SimpleWebCrawlerTool`)的 `execute()` 不再使用 `requests`/`BeautifulSoup`,而是返回 markdown 提示词,指导智能体改用 `run_command` 执行 `curl` 命令完成各 action(`get`/`text`/`title`/`links`/`search`)。`WebPage`、`SimpleWebClient` 类已删除;`urljoin`/`urlparse`/`BeautifulSoup` 导入已清理。代理通过 `get_proxy()` 读取配置并拼出 `--socks5` 参数。注意 `requests` 仍在 `aibox.py` 其他位置(API 调用、DeepSeek 请求)使用,故保留其依赖。 + ### 2.1 SimpleWebClient → curl | 原功能 | 命令行实现 | @@ -111,6 +115,13 @@ ## 阶段三:清理与收尾 +> **实现说明(已落地)**: +> - 3.1 `requirements.txt` 已移除 `beautifulsoup4`;**`requests` 保留**(仍在 `aibox.py` 的 DeepSeek API 调用 `ConversationCompressor` 与 `DeepSeekChat` 中使用,属核心功能,无法移除)。 +> - 3.2 已统一:文件/网页工具改为返回 markdown 提示词,实际 I/O 全部委托给 `run_command`(`SubprocessCommandExecutor`),天然复用其 1MB 输出上限、超时与中断取消。 +> - 3.3 中断支持已随 `SubprocessCommandExecutor.cancel()` 覆盖。 +> - 3.4 默认提示词 `3_file_operations.md`、`4_web_access.md` 已更新为命令行走指导(`head`/`sed`/`cat`/`ls`/`curl` 等)。 +> - 3.5 `README.md` 工具表与安全说明已更新。 + ### 3.1 移除 Python 依赖 从 `requirements.txt` 删除: