sycommon-python-lib 0.2.7a56__py3-none-any.whl → 0.2.8a0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -389,13 +389,15 @@ class FileOperationsMixin:
389
389
  )
390
390
  # 对齐 deepagents 0.7.1 ReadResult 分页字段:服务端按行切片后回传
391
391
  # start_line/end_line/total_lines/next_offset,框架据此渲染分页提示。
392
- # no_lines_requested(limit<=0 早退):服务端返回空 content 且无分页字段。
392
+ # no_lines_requested(limit<=0 早退):服务端返回空 content + 该标志;
393
+ # 透传给 ReadResult,让 FilesystemMiddleware 区分「零行窗口」与「空文件」。
393
394
  return ReadResult(
394
395
  file_data=file_data,
395
396
  start_line=result.get("start_line"),
396
397
  end_line=result.get("end_line"),
397
398
  total_lines=result.get("total_lines"),
398
399
  next_offset=result.get("next_offset"),
400
+ no_lines_requested=bool(result.get("no_lines_requested")),
399
401
  )
400
402
  except Exception as e:
401
403
  SYLogger.error(f"[Sandbox] 读取文件异常: {e}")
@@ -471,12 +473,14 @@ class FileOperationsMixin:
471
473
  encoding=encoding,
472
474
  )
473
475
  # 对齐 deepagents 0.7.1 ReadResult 分页字段(与同步 read 一致)。
476
+ # no_lines_requested 透传:区分零行窗口与空文件(见同步版注释)。
474
477
  return ReadResult(
475
478
  file_data=file_data,
476
479
  start_line=result.get("start_line"),
477
480
  end_line=result.get("end_line"),
478
481
  total_lines=result.get("total_lines"),
479
482
  next_offset=result.get("next_offset"),
483
+ no_lines_requested=bool(result.get("no_lines_requested")),
480
484
  )
481
485
  except Exception as e:
482
486
  SYLogger.error(f"[Sandbox] 异步读取文件异常: {e}")
@@ -60,8 +60,8 @@ class FileDownloadResult:
60
60
  from sycommon.logging.kafka_log import SYLogger
61
61
  from sycommon.agent.sandbox.file_ops import FileOperationsMixin, SANDBOX_API_PREFIX
62
62
 
63
+ from deepagents.backends.sandbox import BaseSandbox
63
64
  from deepagents.backends.protocol import (
64
- SandboxBackendProtocol,
65
65
  ExecuteResponse,
66
66
  FileDownloadResponse,
67
67
  FileUploadResponse,
@@ -219,10 +219,24 @@ def filter_instances_by_owner(instances: list, user_id: str, timeout: float = 3.
219
219
  return filtered
220
220
 
221
221
 
222
- class HTTPSandboxBackend(FileOperationsMixin, SandboxBackendProtocol):
222
+ class HTTPSandboxBackend(FileOperationsMixin, BaseSandbox):
223
223
  """
224
224
  通过 HTTP API 连接远程沙箱容器
225
225
 
226
+ 继承 BaseSandbox(而非 SandboxBackendProtocol)以解锁 deepagents
227
+ capture-at-source offload:execute 大输出在沙箱源端截成 head/tail 预览,
228
+ 完整结果落盘 /large_tool_results/<tool_call_id>,模型可经 read_file 翻页恢复,
229
+ 不必重跑命令。落盘目录由 SandboxRecoveryManager/minio_sync cleanup 兜底清理。
230
+ FileOperationsMixin 在 MRO 最前,read/ls/grep/edit/delete 等仍走本类自定义
231
+ 实现(HTTP 远程调用),BaseSandbox 的 shell 默认实现不会被触发。
232
+ """
233
+
234
+ # capture-at-source offload:opt-in,依赖沙箱镜像含 POSIX sh + head/tail/wc/cat。
235
+ # 生产 Linux 沙箱走 bash heredoc 执行命令(见 _build_execute_command),必然满足。
236
+ enable_capture_offload = True
237
+
238
+ """
239
+
226
240
  支持两种模式:
227
241
  1. 直接 URL: 直接指定沙箱服务地址
228
242
  2. Nacos 服务发现: 通过服务名从 Nacos 获取实例(支持自动故障转移)
@@ -340,6 +340,10 @@ _GREP_TIMEOUT = 60
340
340
  # read 文本路径服务端字节上限(对齐 deepagents _READ_COMMAND_TEMPLATE 的
341
341
  # MAX_OUTPUT_BYTES=500KiB):超大文本文件不会把整包塞进 HTTP 响应/内存。
342
342
  _READ_MAX_OUTPUT_BYTES = 500 * 1024
343
+ # read 提前停止后补数 total_lines 的文件大小闸门(对齐 deepagents sandbox.py:499
344
+ # MAX_LINE_COUNT_BYTES=1MiB):超过此大小的文件不数总行数(total_lines=None),
345
+ # 避免 GB 级文件的逐行计数把 read 拖成慢操作。
346
+ _READ_TOTAL_LINE_MAX_BYTES = 1024 * 1024
343
347
  # 默认工作目录配置
344
348
  # - Linux: /data/sycommon_sandbox (固定路径,自动创建)
345
349
  # - macOS/Windows: 使用系统临时目录
@@ -1027,8 +1031,10 @@ def setup_sandbox_handler(app: FastAPI, config: dict = None):
1027
1031
 
1028
1032
  # limit<=0:协议 no_lines_requested —— 不读文件,返回空内容 + 标志位,
1029
1033
  # 不带任何分页字段(客户端填 ReadResult.no_lines_requested=True)。
1034
+ # 对齐 deepagents _build_read_cmd 的 limit<=0 早退分支。区别于空文件
1035
+ # (空文件走上面的 "File exists but has empty contents" 提示)。
1030
1036
  if limit is not None and limit <= 0:
1031
- return ReadResponse(content="", error=None, encoding="utf-8")
1037
+ return ReadResponse(content="", error=None, encoding="utf-8", no_lines_requested=True)
1032
1038
 
1033
1039
  lines = []
1034
1040
  lines_read = 0
@@ -1082,12 +1088,18 @@ def setup_sandbox_handler(app: FastAPI, config: dict = None):
1082
1088
  next_offset = None
1083
1089
  else:
1084
1090
  # 提前停止(limit 到顶 / 字节截断):需要 total_lines 才能算「剩余」。
1085
- # 大文件全量计数代价高,这里只在截断时补一次剩余行数扫描
1086
- # (对齐 deepagents「total_lines 已知时才报」的做法)。
1087
- try:
1088
- with open(file_path, 'r', encoding='utf-8', newline=None) as f:
1089
- total_lines = sum(1 for _ in f)
1090
- except OSError:
1091
+ # 大文件全量计数代价高——先比 st_size 与 _READ_TOTAL_LINE_MAX_BYTES
1092
+ # (对齐 deepagents sandbox.py:499 的 MAX_LINE_COUNT_BYTES=1MiB 闸门),
1093
+ # 超过则不数(返回 None,next_offset 退化为 end_line),避免无界扫描
1094
+ # 把 read 变成对 GB 级文件的逐行计数慢操作。
1095
+ file_size = os.path.getsize(file_path)
1096
+ if file_size <= _READ_TOTAL_LINE_MAX_BYTES:
1097
+ try:
1098
+ with open(file_path, 'r', encoding='utf-8', newline=None) as f:
1099
+ total_lines = sum(1 for _ in f)
1100
+ except OSError:
1101
+ total_lines = None
1102
+ else:
1091
1103
  total_lines = None
1092
1104
  # next_offset = end_line(0-indexed 下一行 = 1-indexed 末行号),
1093
1105
  # 仅当确实还有后续行时设置;否则视为读到末尾。
@@ -1724,19 +1736,65 @@ def setup_sandbox_handler(app: FastAPI, config: dict = None):
1724
1736
  grep_opts = "-rHnFZ" if use_null_separator else "-rHnF"
1725
1737
 
1726
1738
  # 安全处理 glob_pattern,使用 shlex.quote 防止命令注入
1727
- glob_pattern = f"--include={shlex.quote(req.glob)}" if req.glob else ""
1728
1739
  pattern_escaped = shlex.quote(req.pattern)
1729
1740
 
1730
- # 注意:不再用 2>/dev/null || true,以便捕获 stderr 用于错误诊断
1731
- cmd = f"grep {grep_opts} {glob_pattern} -e {pattern_escaped} {quoted_path}"
1741
+ # glob 含 '/'(如 src/**/*.py)时,GNU grep --include 只匹配 basename,
1742
+ # 会静默返回 0 条(对齐 deepagents sandbox.py:684 _build_grep_cmd 的判定)。
1743
+ # 这种路径 glob 改走 Python glob 预筛文件清单 + grep -F 逐文件/批量,
1744
+ # 避免「带路径 glob 搜不到东西」的隐蔽 bug。
1745
+ path_glob = req.glob
1746
+ use_path_glob = bool(path_glob and "/" in path_glob)
1747
+ if not use_path_glob:
1748
+ glob_pattern = f"--include={shlex.quote(req.glob)}" if req.glob else ""
1749
+ # 注意:不再用 2>/dev/null || true,以便捕获 stderr 用于错误诊断
1750
+ cmd = f"grep {grep_opts} {glob_pattern} -e {pattern_escaped} {quoted_path}"
1751
+ else:
1752
+ glob_pattern = "" # 不用 --include;文件清单由 Python glob 预筛后喂给 grep
1753
+
1732
1754
  # 对齐 deepagents 0.7.1 max_count:服务端用 head 早停,多取 1 条用于判断是否截断。
1733
1755
  # (grep -r 没有真正的全局计数上限,head -n N+1 截 stdout 后,客户端按 N 裁剪、
1734
1756
  # 超过即 truncated=True。与 deepagents BaseSandbox._build_grep_cmd 同款思路。)
1735
1757
  max_count = req.max_count
1736
- if max_count is not None and max_count >= 0:
1758
+ if not use_path_glob and max_count is not None and max_count >= 0:
1737
1759
  # || true 兜底 SIGPIPE(head 读够后关闭管道,grep 收到 SIGPIPE 退出码非 0)
1738
1760
  cmd = f"{cmd} | head -n {int(max_count) + 1} || true"
1739
1761
 
1762
+ # 含路径 glob 的分支:Python glob 预筛出匹配文件清单(仅文件,递归),
1763
+ # 经 xargs 批量喂给 grep -F。沿用 -Z/-Hn 与正文相同的 NUL/冒号解析,故
1764
+ # 下游 _parse_grep_results 无需改动。文件清单为空时直接返回 0 条,避免起
1765
+ # `grep -e pat`(无文件参数)误把 stdin 当搜索源。
1766
+ def _resolve_path_glob_files():
1767
+ import glob as glob_module
1768
+ full_pattern = os.path.join(search_path, path_glob)
1769
+ try:
1770
+ files = sorted(glob_module.glob(full_pattern, recursive=True))
1771
+ except (FileNotFoundError, NotADirectoryError, PermissionError) as e:
1772
+ SYLogger.warning(f"[Sandbox Server] grep 路径 glob 异常: {e}")
1773
+ return []
1774
+ workspace_resolved_g = os.path.realpath(workspace)
1775
+ safe = []
1776
+ for m in files:
1777
+ if os.path.isdir(m):
1778
+ continue # grep 按文件搜,目录跳过
1779
+ try:
1780
+ if not _is_path_within(m, workspace_resolved_g):
1781
+ continue
1782
+ except OSError:
1783
+ continue
1784
+ safe.append(m)
1785
+ return safe
1786
+
1787
+ if use_path_glob:
1788
+ path_glob_files = await asyncio.to_thread(_resolve_path_glob_files)
1789
+ if not path_glob_files:
1790
+ return GrepResponse(matches=[])
1791
+ # xargs -0:文件名以 NUL 分隔喂入,文件名含空格/引号/换行也安全。
1792
+ # find ... -print0 | grep -FZne pattern --files-from=... 也行,但 xargs 最 portable。
1793
+ files_argv = " ".join(shlex.quote(f) for f in path_glob_files)
1794
+ # 不加 -r(已显式给文件清单,无需递归);保留 -H(始终带文件名) -n(行号) -F(字面量) -Z(NUL)。
1795
+ path_glob_grep_opts = "-HnFZ" if use_null_separator else "-HnF"
1796
+ cmd = f"printf '%s\\0' {files_argv} | xargs -0 grep {path_glob_grep_opts} -e {pattern_escaped}"
1797
+
1740
1798
  # 使用 asyncio.create_subprocess_shell 异步执行
1741
1799
  proc = await asyncio.create_subprocess_shell(
1742
1800
  cmd,
@@ -1831,6 +1889,7 @@ def setup_sandbox_handler(app: FastAPI, config: dict = None):
1831
1889
 
1832
1890
  matches = await asyncio.to_thread(_parse_grep_results)
1833
1891
  # 对齐 deepagents 0.7.1:超出 max_count 即置 truncated(多取的 1 条在这里裁掉)。
1892
+ # 路径 glob 分支没在服务端 head 早停(直接 grep 文件清单),这里统一兜底裁剪。
1834
1893
  truncated = False
1835
1894
  if max_count is not None and len(matches) > max_count:
1836
1895
  matches = matches[:max_count]
@@ -143,6 +143,11 @@ class ReadResponse(BaseModel):
143
143
  end_line: Optional[int] = None
144
144
  total_lines: Optional[int] = None
145
145
  next_offset: Optional[int] = None
146
+ # limit<=0 早退标志(对齐 deepagents ReadResult.no_lines_requested):
147
+ # True 表示「文件未检视、零行窗口」,与「空文件」区分开。客户端据此
148
+ # 填 ReadResult.no_lines_requested,让 FilesystemMiddleware 给出与框架
149
+ # 合成实现一致的分页提示(NO_LINES_REQUESTED_WARNING)而非空文件提示。
150
+ no_lines_requested: bool = False
146
151
 
147
152
 
148
153
  class WriteRequest(BaseModel):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sycommon-python-lib
3
- Version: 0.2.7a56
3
+ Version: 0.2.8a0
4
4
  Summary: Add your description here
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -9,11 +9,11 @@ Requires-Dist: aiohttp==3.14.3
9
9
  Requires-Dist: aiomysql==0.3.2
10
10
  Requires-Dist: anyio==4.14.2
11
11
  Requires-Dist: decorator==5.3.1
12
- Requires-Dist: deepagents==0.7.1
13
- Requires-Dist: elasticsearch==9.4.1
12
+ Requires-Dist: deepagents==0.7.4
13
+ Requires-Dist: elasticsearch==9.5.0
14
14
  Requires-Dist: fastapi==0.141.1
15
15
  Requires-Dist: jinja2==3.1.6
16
- Requires-Dist: kafka-python==3.0.9
16
+ Requires-Dist: kafka-python==3.0.10
17
17
  Requires-Dist: langchain==1.3.14
18
18
  Requires-Dist: langchain-core==1.5.3
19
19
  Requires-Dist: langchain-openai==1.4.1
@@ -158,8 +158,8 @@ sycommon/agent/middleware/skill_wl_check.py,sha256=rCJ9F6aWPh8tVQBIPmcq2lKDsfwiJ
158
158
  sycommon/agent/middleware/skill_write_guard.py,sha256=ZLW-OIG56z5LvLRMaMnryhPz_O5PbrdTLuHTfC3PveA,3101
159
159
  sycommon/agent/sandbox/__init__.py,sha256=jR7LlkD4J4Y6QYyRXQClkwmqDBCCPmycV_hQV9p9YHw,4621
160
160
  sycommon/agent/sandbox/consistent_hash.py,sha256=8Jgk-W4NAD2-u5_vKRVlPmql_e0Vy4aNORiXOuzvIrs,7277
161
- sycommon/agent/sandbox/file_ops.py,sha256=yC7aLhDN6Wags_bgTPWnIuAX7eGJcRgB6DHRVdqtY9Y,42582
162
- sycommon/agent/sandbox/http_sandbox_backend.py,sha256=ig0Ey8xEuLpT40ZouQ_rkdjmZU5qGILILFe8hLGfOf0,89703
161
+ sycommon/agent/sandbox/file_ops.py,sha256=aZvNZMKXR0lbkE5jFc3CDf87Q0mJTO26EYY1U1RxHBs,42933
162
+ sycommon/agent/sandbox/http_sandbox_backend.py,sha256=aqi6VEwGjIQbChx9iKIIwpiLmEXkGtW-3Ta5pDnVVns,90517
163
163
  sycommon/agent/sandbox/minio_sync.py,sha256=ORAGoRoXM54Zc49h_MkRo-SVa_X4o3_pkdD-vAViEsU,45529
164
164
  sycommon/agent/sandbox/sandbox_pool.py,sha256=eMn8sLakCWf90l6ni2-333QM8oBdX1CflV-WzneFp_k,9133
165
165
  sycommon/agent/sandbox/sandbox_recovery.py,sha256=X-eDODx1tmGMh_iTngV6e1ppfDBHpTdkPreJusN5MHY,7358
@@ -237,7 +237,7 @@ sycommon/middleware/exception.py,sha256=UAy0tKijI_2JoKjwT3h62aL-tybftP3IETvcr26N
237
237
  sycommon/middleware/middleware.py,sha256=1qA_0rT7pv1xTfuvA0kdYkxVOHYT6roSSNO7Cmf2wYg,1778
238
238
  sycommon/middleware/monitor_memory.py,sha256=pYRK-wRuDd6enSg9Pf8tQxPdYQS6S0AyjyXeKFRLKEs,628
239
239
  sycommon/middleware/mq.py,sha256=9X6KKtadFjBXKS5L3kEKujYio9wwGfWgXwWOAHO-HDg,254
240
- sycommon/middleware/sandbox.py,sha256=D5aO-hndmjfiGAuJFPXhr5cKgea0yxQU-RWvS9GDxis,87973
240
+ sycommon/middleware/sandbox.py,sha256=gkISNE9klC16XQjr7LzXIASr2BfvEwlpHCz7r5Mhjz4,91701
241
241
  sycommon/middleware/timeout.py,sha256=KlxOPa8xl2dg6yuRi_EzkVJG8bX4stb5ueYxctzzGM8,1433
242
242
  sycommon/middleware/token_tracking.py,sha256=rEbgV1bgWMdzAERx4aq5XAvOIT6jTY_tK1P0xHJnL3o,6609
243
243
  sycommon/middleware/tool_result_truncation.py,sha256=dTh2VHyNVGvl7CyjsZ4SqAxaabR5hnL-oB0bf2-S9Pk,12534
@@ -249,7 +249,7 @@ sycommon/models/mcp_server_config.py,sha256=cm_oDEDXVJR4nCKgWPli5zFI0DUy6xtvRs-_
249
249
  sycommon/models/mqlistener_config.py,sha256=ZFGg_vc9HcIV22L5Jgrynkjj5B6Ci8ETQNUbNUiXrVw,1830
250
250
  sycommon/models/mqmsg_model.py,sha256=Zo-LsDMFuF1Vkx9ZmwBC9E7TrCw-7nAQD2TE4v9-6F4,291
251
251
  sycommon/models/mqsend_config.py,sha256=NQX9dc8PpuquMG36GCVhJe8omAW1KVXXqr6lSRU6D7I,268
252
- sycommon/models/sandbox.py,sha256=I0zWmSIBXwS1GiGNt88n0XSBOGjFPs3zETWcr3xylPQ,8371
252
+ sycommon/models/sandbox.py,sha256=wnvLtBrwO4N6dNe1d78elLnr3dii-Gh08m0bDoLQo0g,8773
253
253
  sycommon/models/sso_user.py,sha256=uqLlZevVaDQQche5eBq8m1b8lu-45Pe4vHY3kd3NdxA,2896
254
254
  sycommon/models/token_usage.py,sha256=w3Aash7jUVSUIgKxBUzI6SqJ9VV5Pk7rLBbMUlLv6nA,1242
255
255
  sycommon/models/token_usage_mysql.py,sha256=eWnCvI2FDwyTA_fr4wWQtAmUnFJgLwrt0WXMXHLAESQ,2633
@@ -326,8 +326,8 @@ sycommon/tools/timing.py,sha256=OiiE7P07lRoMzX9kzb8sZU9cDb0zNnqIlY5pWqHcnkY,2064
326
326
  sycommon/tools/user_id.py,sha256=o-zsubGSR5rSSZf8Pk8B7AhE21ahDTTIW5JZmJO6Flw,4761
327
327
  sycommon/xxljob/__init__.py,sha256=7eoBlQxv-B39IfRSCY2bkqdGYs1QRe1umAWd88VMEEM,86
328
328
  sycommon/xxljob/xxljob_service.py,sha256=1yifwIBNGsCIxLnQjHKiBlbsigc_zvPH-dMTZcNxe-Q,7649
329
- sycommon_python_lib-0.2.7a56.dist-info/METADATA,sha256=iVq951x5V-tHurfa8px8naXAy7oWeTDb2qaJxikujHA,7996
330
- sycommon_python_lib-0.2.7a56.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
331
- sycommon_python_lib-0.2.7a56.dist-info/entry_points.txt,sha256=gsR4SssKxDWjRU8ggidzNcdMXDPRSKRS7UaGyNP84Qg,92
332
- sycommon_python_lib-0.2.7a56.dist-info/top_level.txt,sha256=RgphKrg7nJyZ7irJqbxFr-5H2LUYTvI7ivoWZH2hcD0,29
333
- sycommon_python_lib-0.2.7a56.dist-info/RECORD,,
329
+ sycommon_python_lib-0.2.8a0.dist-info/METADATA,sha256=1M3hc17dUZxai8jTTPmuAAWUFgUGg4ULeYkuC7z72fk,7996
330
+ sycommon_python_lib-0.2.8a0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
331
+ sycommon_python_lib-0.2.8a0.dist-info/entry_points.txt,sha256=gsR4SssKxDWjRU8ggidzNcdMXDPRSKRS7UaGyNP84Qg,92
332
+ sycommon_python_lib-0.2.8a0.dist-info/top_level.txt,sha256=RgphKrg7nJyZ7irJqbxFr-5H2LUYTvI7ivoWZH2hcD0,29
333
+ sycommon_python_lib-0.2.8a0.dist-info/RECORD,,