Skip to content

fix(code-executors): preserve collected binary files - #344

Merged
weimch merged 1 commit into
mainfrom
fix/issue-316
Sep 23, 2026
Merged

weimch merged 1 commit into
mainfrom
fix/issue-316

Conversation

@raychen911

Copy link
Copy Markdown
Contributor
  • 修复二进制输出问题

@helloopenworld

Copy link
Copy Markdown
Contributor

AI Code Review

审查结论

不通过

审查范围:b80d43a..17bd845(fix(code-executors): preserve collected binary files),5 个变更文件(2 个 SDK 源文件 + 3 个测试文件),84 增 18 删。计划意图:消除 collect()/collect_outputs() 对二进制文件用 utf-8 replace 解码造成的字节破坏,改为 content_base64 + get_bytes() 无损表达。实现层面(_encode_collected_content 双表示、CodeFile/ManifestFileRef 各增 get_bytes、3 个新测试)正确;三个后端(local/container/cube)的 collect/collect_outputs 均路由到被修复的共享辅助函数,无残留 errors=replace 的 collect 路径。核心缺陷:修复只做到编码端,全仓 grep 证实 get_bytes()/content_base64 除测试外零调用——skill_run 的 _prepare_outputs 重建 CodeFile 丢弃 content_base64、_to_run_file 与 _save_artifacts 只读 .content、三个 put_files 后端仍写 file.content or b'',二进制字节在所有产品路径上依旧静默丢失,text/* MIME 的非 UTF-8 文本从"乱码可见"退化为"内容全空",collect→put 往返写出 0 字节文件;新 API 另有 validate=True 解码崩溃与截断文件 size_bytes 不一致两个边界缺陷;测试只覆盖共享辅助函数与小文件,未覆盖任何消费端、截断与 save_as_artifacts 二进制分支。门禁结论:FAILED。

发现的问题

严重

trpc_agent_sdk/code_executors/_base_workspace_runtime.py:171-180

问题: 本修复只覆盖"编码端",没有任何消费端读取 content_base64 或调用 get_bytes()(全仓库 grep 证实生产代码零调用):_skill_run.py_to_run_file(约 601 行)只拷贝 f.content_prepare_outputs(855-866 行)把 ManifestFileRef 重建为 CodeFile 时不复制 content_base64(该字段落回默认空串),_save_artifacts(915 行)对空的 f.contentencode('utf-8'),三个后端的 put_files(cube _runtime.py:159、local _local_ws_runtime.py:490、container _container_ws_runtime.py:631)仍写 file.content or b''。因此本变更新增的"无损"通道在产品路径上全部失效。

触发条件: 收集任何非 UTF-8 文件(PNG/PDF/GBK 编码文本等),并发生以下任一情况:(a) 经 skill_run 的 output_filesoutputs.inline 返回给模型;(b) 把 collect 结果通过 put_files 写回 workspace(collect 到 put 往返);(c) 工具开启 save_as_artifacts

实际影响: 二进制字节在每一条产品路径上依旧静默丢失,且比改动前更隐蔽:text/* MIME 的非 UTF-8 文本(如 GBK 编码)从"乱码但可见"退化为"内容全空";collect→put 往返写出 0 字节文件而 size_bytes 仍报告真实大小;save_as_artifacts 会把二进制保存为空 artifact;新增的 get_bytes() API 无任何调用者,字节完全不可恢复,违背了该提交"preserve collected binary files"的核心目标。

修正方向:_skill_run.py_to_run_file/_prepare_outputs 中透传 content_base64(给 SkillRunFile 增加该字段并在取字节处使用 get_bytes()),将 _save_artifacts 改为 Part.from_bytes(data=f.get_bytes() ...)(或对应 SkillRunFile 的字节访问器),并让三个 put_files 后端在 content 为空时回退解码 content_base64

中等

trpc_agent_sdk/code_executors/_types.py:40-44

问题: get_bytes() 使用 base64.b64decode(self.content_base64, validate=True)validate=True 在遇到非标准 base64 字符、缺 = 填充或含空白/换行(如经过 JSON 重新格式化、URL-safe 变体或手工构造)时直接抛出 binascii.ErrorValueError 子类),且没有任何调用方捕获。content_base64 是新增的公开模型字段,CodeFile/ManifestFileRef 均可由外部按 JSON 反序列化构造。

触发条件: 调用方以 CodeFile.model_validate(json.loads(...)) 或手工构造方式传入非规范 base64(未填充、含换行、urlsafe_b64encode 输出),随后调用 get_bytes()。内部 _encode_collected_content 用标准 b64encode(始终填充、无空白)不受影响,但该路径恰好覆盖不了外部构造的取值。

实际影响: 二进制恢复的唯一公开 API 在输入非规范 base64 时抛异常而非返回字节,调用方(agent 工具循环、JSON 往返)崩溃;与"无损恢复"的契约相悖。

修正方向: 去掉 validate=Trueb64decode 默认可容忍空白并对非法字符抛错仍可捕获),或先做 re.sub(r'\s+', '', s) 规范化,并对 binascii.Error 给出明确的 ValueError 与降级路径;同时为非法输入补充单元测试。

中等

trpc_agent_sdk/code_executors/_base_workspace_runtime.py:60-65

问题: _encode_collected_content 用严格 data.decode('utf-8') 判定"文本 vs 二进制",仅凭 UTF-8 合法性做界:含单个非法字节的文本文件整体落入 base64 分支(content 变空串);而合法 UTF-8 的二进制(含 NUL 的 UTF-16、全 ASCII 二进制等)又留在 content 里。旧行为是 errors='replace' 保证任意数据都有可显示的 content

触发条件: (a) 收集含一个坏字节的文本文件(日志/CSV 混入 0xFF);(b) 收集合法 UTF-8 但本质为二进制的文件并被 text/* 误判 MIME。

实际影响: 场景 (a) 下此前模型可见的文本(仅一个 U+FFFD)现在整文件变成空串加 base64 块,文本内容在 prompt/工具结果中消失(_should_inline_file_content 恰好在 content 为空时返回 True,使空内容被"内联");场景 (b) 下 NUL 字符字符串进入 content,依赖 content 纯文本的消费者(拼 prompt、日志)处理到裸 NUL/控制字符。

修正方向: 以"内容是否可以安全作为文本呈现"(如结合 MIME 与 NUL/控制字符检测)替代单纯的 UTF-8 合法性判定,或至少对截断文件(raw_size > len(data))显式走 base64 分支;并为单坏字节文本文件补测试。

中等

trpc_agent_sdk/code_executors/_types.py:40-44

问题: get_bytes() 返回的是截断后前缀(content_base64 只编码了按 max_read_size(4 MiB)读取的前缀),而 size_bytes 报告完整 raw_sizeCodeFile.truncated 为 True 时二者不一致,且没有任何消费方在使用 get_bytes() 前检查 truncated

触发条件: 收集超过 4 MiB 的二进制文件(cube/local/container 的 fetch 都按上限截取),随后调用方按 size_bytes 或按"base64 解码即完整数据"的假设处理 get_bytes() 结果(哈希、上传、artifact 保存)。

实际影响: 新公开的"无损"API 对超限文件静默返回部分字节:base64 本身解码成功、无任何异常,调用方得到与 size_bytes 不符的截断数据且无从察觉(truncated 标志被忽略),可能将半截文件写入 artifact 或下游存储。

修正方向: get_bytes() 至少在 truncated=True 时抛出可识别的异常或返回 (bytes, truncated) 元组,并把"截断文件"分支纳入 test_base_workspace_fs_collect.py 的覆盖(现有新增测试只覆盖小文件)。

较低

tests/code_executors/cube/test_bug_hunt.py:455-459

问题: 新增测试只断言了共享辅助函数与"编码端"行为(content_base64 非空、get_bytes() 等于原字节),没有覆盖任何消费端(_skill_run.py_to_run_file/_prepare_outputs/_save_artifacts、三个后端 put_files),而消费端恰恰全部未迁移(见 _base_workspace_runtime.py:171 的严重问题),因此测试套件全绿也无法发现"收集无损但产品路径丢失字节"的缺陷;test_bug9 还断言了 content == '' 这一实现细节(未来若改为对二进制仍填充非空 content 会误报失败)。

触发条件: 运行 test_bug9_collect_preserves_binary_bytes 及相关新增测试时全部通过,但真实 skill_run/跨 workspace 拷贝场景仍丢字节。

实际影响: 测试给出"修复完成"的误导性信号,掩盖本提交未达成的核心目标;save_as_artifacts 二进制、截断二进制、collect→put 往返均无测试。

修正方向: 增加端到端测试:skill_run outputs.inline 收集二进制并断言 SkillRunOutput.output_files 可恢复字节(修复 _skill_run.py 后再加);cube put_files 往返测试;save_as_artifacts 二进制文件测试;同时把断言从 content == '' 改为行为不变量(get_bytes() == binary)。

Comment on lines +40 to +44
def get_bytes(self) -> bytes:
"""Return the collected content without losing binary bytes."""
if self.content_base64:
return base64.b64decode(self.content_base64, validate=True)
return self.content.encode("utf-8")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

问题: get_bytes() 使用 base64.b64decode(self.content_base64, validate=True)validate=True 在遇到非标准 base64 字符、缺 = 填充或含空白/换行(如经过 JSON 重新格式化、URL-safe 变体或手工构造)时直接抛出 binascii.ErrorValueError 子类),且没有任何调用方捕获。content_base64 是新增的公开模型字段,CodeFile/ManifestFileRef 均可由外部按 JSON 反序列化构造。

触发条件: 调用方以 CodeFile.model_validate(json.loads(...)) 或手工构造方式传入非规范 base64(未填充、含换行、urlsafe_b64encode 输出),随后调用 get_bytes()。内部 _encode_collected_content 用标准 b64encode(始终填充、无空白)不受影响,但该路径恰好覆盖不了外部构造的取值。

实际影响: 二进制恢复的唯一公开 API 在输入非规范 base64 时抛异常而非返回字节,调用方(agent 工具循环、JSON 往返)崩溃;与"无损恢复"的契约相悖。

修正方向: 去掉 validate=Trueb64decode 默认可容忍空白并对非法字符抛错仍可捕获),或先做 re.sub(r'\s+', '', s) 规范化,并对 binascii.Error 给出明确的 ValueError 与降级路径;同时为非法输入补充单元测试。

Comment on lines +60 to +65
def _encode_collected_content(data: bytes) -> Tuple[str, str]:
"""Return lossless text and Base64 representations for collected bytes."""
try:
return data.decode("utf-8"), ""
except UnicodeDecodeError:
return "", base64.b64encode(data).decode("ascii")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

问题: _encode_collected_content 用严格 data.decode('utf-8') 判定"文本 vs 二进制",仅凭 UTF-8 合法性做界:含单个非法字节的文本文件整体落入 base64 分支(content 变空串);而合法 UTF-8 的二进制(含 NUL 的 UTF-16、全 ASCII 二进制等)又留在 content 里。旧行为是 errors='replace' 保证任意数据都有可显示的 content

触发条件: (a) 收集含一个坏字节的文本文件(日志/CSV 混入 0xFF);(b) 收集合法 UTF-8 但本质为二进制的文件并被 text/* 误判 MIME。

实际影响: 场景 (a) 下此前模型可见的文本(仅一个 U+FFFD)现在整文件变成空串加 base64 块,文本内容在 prompt/工具结果中消失(_should_inline_file_content 恰好在 content 为空时返回 True,使空内容被"内联");场景 (b) 下 NUL 字符字符串进入 content,依赖 content 纯文本的消费者(拼 prompt、日志)处理到裸 NUL/控制字符。

修正方向: 以"内容是否可以安全作为文本呈现"(如结合 MIME 与 NUL/控制字符检测)替代单纯的 UTF-8 合法性判定,或至少对截断文件(raw_size > len(data))显式走 base64 分支;并为单坏字节文本文件补测试。

Comment on lines +40 to +44
def get_bytes(self) -> bytes:
"""Return the collected content without losing binary bytes."""
if self.content_base64:
return base64.b64decode(self.content_base64, validate=True)
return self.content.encode("utf-8")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

问题: get_bytes() 返回的是截断后前缀(content_base64 只编码了按 max_read_size(4 MiB)读取的前缀),而 size_bytes 报告完整 raw_sizeCodeFile.truncated 为 True 时二者不一致,且没有任何消费方在使用 get_bytes() 前检查 truncated

触发条件: 收集超过 4 MiB 的二进制文件(cube/local/container 的 fetch 都按上限截取),随后调用方按 size_bytes 或按"base64 解码即完整数据"的假设处理 get_bytes() 结果(哈希、上传、artifact 保存)。

实际影响: 新公开的"无损"API 对超限文件静默返回部分字节:base64 本身解码成功、无任何异常,调用方得到与 size_bytes 不符的截断数据且无从察觉(truncated 标志被忽略),可能将半截文件写入 artifact 或下游存储。

修正方向: get_bytes() 至少在 truncated=True 时抛出可识别的异常或返回 (bytes, truncated) 元组,并把"截断文件"分支纳入 test_base_workspace_fs_collect.py 的覆盖(现有新增测试只覆盖小文件)。

Comment on lines 455 to +459
files = await fs.collect(ws, ["*.png"])
assert len(files) == 1
# The raw bytes should be recoverable. They are not: utf-8
# replace turns \x80 into U+FFFD, and re-encoding does not roundtrip.
assert files[0].content.encode("utf-8") == binary, (
"binary file silently corrupted by utf-8 replace"
)
assert files[0].content == ""
assert files[0].content_base64
assert files[0].get_bytes() == binary

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

问题: 新增测试只断言了共享辅助函数与"编码端"行为(content_base64 非空、get_bytes() 等于原字节),没有覆盖任何消费端(_skill_run.py_to_run_file/_prepare_outputs/_save_artifacts、三个后端 put_files),而消费端恰恰全部未迁移(见 _base_workspace_runtime.py:171 的严重问题),因此测试套件全绿也无法发现"收集无损但产品路径丢失字节"的缺陷;test_bug9 还断言了 content == '' 这一实现细节(未来若改为对二进制仍填充非空 content 会误报失败)。

触发条件: 运行 test_bug9_collect_preserves_binary_bytes 及相关新增测试时全部通过,但真实 skill_run/跨 workspace 拷贝场景仍丢字节。

实际影响: 测试给出"修复完成"的误导性信号,掩盖本提交未达成的核心目标;save_as_artifacts 二进制、截断二进制、collect→put 往返均无测试。

修正方向: 增加端到端测试:skill_run outputs.inline 收集二进制并断言 SkillRunOutput.output_files 可恢复字节(修复 _skill_run.py 后再加);cube put_files 往返测试;save_as_artifacts 二进制文件测试;同时把断言从 content == '' 改为行为不变量(get_bytes() == binary)。

@weimch weimch left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approve

@weimch
weimch merged commit 886025d into main Sep 23, 2026
5 checks passed
@helloopenworld

Copy link
Copy Markdown
Contributor

AI Code Review

审查结论

通过

审查范围:b80d43a..7a0af21(fix(code-executors): preserve collected binary files),7 个非生成文件:_base_workspace_runtime.py_types.py_skill_run.py 及 4 个测试文件。

计划符合性:核心目标在收集层达成——collect()/collect_outputs() 通过 content_base64 + get_bytes() 对非 UTF-8 文件实现无损字节保留,配套测试(base64 编解码、get_bytes 往返、inline 分支、_should_inline_file_content 守卫)覆盖了收集层的主要成功/边界路径,均非空泛断言。

主要风险:变更引入的行为回归集中在 skill_run 消费层。(1)_encode_collected_content 把"非 UTF-8"等同于"二进制",导致文本 MIME 但含非 UTF-8 字节的文件(latin-1/GBK 文本、或读取上限处被截断在多字节字符中间的 >4MiB 文本)从原先内联可见的部分文本变为 content="";(2)content_base64SkillRunFile 边界被丢弃(该模型无此字段),新加的无损能力在所有工具响应面不可达,get_bytes() 无任何生产调用方;(3)get_bytes() 对外部构造的畸形 base64(validate=True)会抛出未捕获的 binascii.Error,且对 truncated 文件静默返回截断前缀(ManifestFileRefsize_bytes/truncated 字段)。

测试充分性:收集层新增测试充分,但 _skill_run.py 的守卫与传播改动(第 131 行、第 858-867 行)没有任何测试覆盖,上述回归路径未被测试捕获。

门禁结论:无高置信的 SEVERE 级缺陷(经典二进制文件在内联被遮挡/空产物保存方面在基线版本即已存在,非本变更引入);存在 2 个 MODERATE 级回归和 3 个 LOW 级问题,均给出可执行修正方向。

发现的问题

中等

trpc_agent_sdk/code_executors/_base_workspace_runtime.py:60-65

问题: _encode_collected_content 以"能否通过严格 UTF-8 解码"作为二进制判定标准,把所有解码失败的内容(包括文本文件)路由进 content="" + content_base64,而下游 _should_inline_file_content 的新守卫(_skill_run.py:131)对任何设置了 content_base64 的文件直接返回 False,导致原本内联可见的文本内容从 skill_run 响应中消失。

触发条件: ① 文本 MIME 文件含非 UTF-8 字节(如 GBK/latin-1 编码的 .csv/.txtdetect_content_type 按扩展名返回 text/plain);② 超过 MAX_READ_SIZE_BYTES(4 MiB)的 UTF-8 文本文件被 fetcher 按字节截断(local read_bytes()[:max_bytes]、cube data[:max_bytes]、container f.read(max_bytes)),截断点落在一个多字节字符中间导致解码失败。

实际影响: 变更前这些文件的 content 为可读的部分文本(含 U+FFFD 替换符),模型可看到内容;变更后 skill_runoutput_files[].contentprimary_output 均为空,模型把有效文本文件误读为空文件,且 _filter_failed_empty_outputs(失败运行时 f.content 为空 + declarative 路径 size_bytes 恒为 0)会直接丢弃该文件并提示"empty outputs"。

修正方向: 不再以解码成败判定二进制:对解码失败的文本 MIME 文件保留 errors="replace" 的部分文本到 content 同时附加 content_base64 供无损恢复;或在 _should_inline_file_content 中当 content 为空但 content_base64 非空且 MIME 为文本类型时,改为允许内联 base64 解码后的内容。

中等

trpc_agent_sdk/skills/tools/_skill_run.py:129-139

问题: 本变更新增的 content_base64 表示在工具响应边界被丢弃:_prepare_outputs:860-867)把 fr.content_base64 转发进临时 CodeFile,但 _to_run_file:603-612)构造 SkillRunFile 时既因新守卫(:131,content_base64 非空即返回 False)把 content 置空,又因 SkillRunFile:297-308)没有 content_base64 字段而无法携带;全仓库 get_bytes() 无任何生产调用方,该能力在唯一可见的用户输出面上完全不可达。

触发条件: 任意经 skill_run 收集二进制文件的路径:output_files 收集、declarative outputs.inline=True、auto-export out/**;以及 save_as_artifacts=True_save_artifacts:918)以 f.content.encode("utf-8") 为数据源。

实际影响: 工具返回的 SkillRunFile 内容恒为空,模型/调用方无法获得任何字节(workspace:// ref 需在会话内二次解析);对文本 MIME 但含非 UTF-8 字节的文件,save_as_artifacts 从变更前保存含替换字符的非空内容退化为保存 0 字节空产物,用户按 artifact://name@version 取回的是空文件——本变更宣称的"preserve collected binary files"在 skill_run 层未生效。

修正方向:SkillRunFile 增加 content_base64 字段并透传原始字节,或在 _to_run_file 中保留 content 并让 get_bytes() 成为唯一解码入口;同时让 _save_artifacts 对二进制文件保存 get_bytes() 结果而非空串。

较低

trpc_agent_sdk/code_executors/_types.py:40-44

问题: 新增的 get_bytes()CodeFileManifestFileRef 双份实现)使用 base64.b64decode(self.content_base64, validate=True):对任何缺 padding(如 'gA')或含非 base64 字母字符的输入抛出未捕获的 binascii.Error;且对 truncated=True 的文件静默返回被截断的字节前缀,无任何提示——ManifestFileRef 没有 truncated/size_bytes 字段,declarative 路径上 getattr(fr, "truncated", False) 恒为 False。

触发条件: CodeFile/ManifestFileRef 是公开导出的 SDK 类型(code_executors/__init__.py),外部调用方从协议数据/JSON 反序列化时构造了非规范 base64(如 JSON 序列化中 = 被转义、换行折叠、截断的流式 payload),或调用 get_bytes() 恢复一个被 4 MiB 上限截断的文件。

实际影响: 前者直接抛出 binascii.Error: Incorrect padding 使调用方崩溃(内部 b64encode 产物总是规范 padding,故仅外部构造可达);后者静默返回不完整字节,按 get_bytes() 重建的文件损坏且无法感知截断。

修正方向:content_base64 增加 pydantic 校验/规范化(或 get_bytes() 内捕获 binascii.Error 并转为可读错误),并在 ManifestFileRef 补齐 truncated/size_bytes 字段或在 get_bytes() 的文档/返回中显式暴露截断状态;建议两处 get_bytes() 抽为共享函数避免双份漂移。

较低

trpc_agent_sdk/code_executors/_base_workspace_runtime.py:171-176

问题: _build_code_files(collect() 路径)对每个非 UTF-8 文件无条件执行 base64 编码(_base_workspace_runtime.py:171),即使调用方从不读取内容(skill_run 的 _to_run_file 立即丢弃);而 _build_manifest_output:288)做到了仅在 spec.inline 时编码。同类逻辑却不对称。

触发条件: 任意产生二进制文件的 collect() 调用——默认最多 100 个文件、每个上限 4 MiB,单文件 base64 结果约 5.6 MiB 的 str 常驻 CodeFile 直至请求结束。

实际影响: 每个二进制文件多出约 1.33 倍字节的无效编码工作与峰值内存(编码期间同时持有原始 bytes 与 base64 str),在批量收集(如 auto-export 20 个文件)时放大为数百 MiB 的临时内存占用;同时 total_bytes 预算按原始字节统计,模型侧实际驻留的 base64 文本约超出 1.33 倍。

修正方向:_build_manifest_output 对齐:为 _build_code_files 增加惰性编码(如提供 content_base64: Optional[str] = None 由消费方按需解码),或在收集层不生成 base64、仅保留原始 bytes 的 get_bytes() 来源。

较低

trpc_agent_sdk/skills/tools/_skill_run.py:858-868

问题: 本变更对 _skill_run.py 的消费层改动(:131 新守卫、:858-867content_base64 透传)没有任何测试覆盖:4 个新增测试文件全部只测收集层(_build_code_files/_build_manifest_output/CodeFile.get_bytes()/_should_inline_file_content 单元行为),未测 skill_run 端到端路径——文本 MIME 非 UTF-8 文件经 output_files/outputs.inline 收集后返回内容的回归、以及 save_as_artifacts 对二进制文件的空产物问题,均未被测试捕获。

触发条件: 需要新增覆盖场景:① CodeFile(content="", content_base64=...)_to_run_file 后的 SkillRunFile 内容断言;② save_as_artifacts=True + 二进制文件时 artifact 保存字节数断言;③ 声明式 outputs 路径下 _filter_failed_empty_outputs 对失败运行的二进制输出保留行为。

实际影响: 上述 MODERATE 级回归(文本内容消失、空产物保存)当前无任何自动测试兜底,后续改动极易将其固化或再次引入。

修正方向:tests/skills/tools/test_skill_run.py 补充针对 _prepare_outputs_to_run_file_save_artifacts 链路的端到端断言,覆盖二进制与文本 MIME 两类文件的返回内容、ref 与 artifact 字节完整性。

Comment on lines +60 to +65
def _encode_collected_content(data: bytes) -> Tuple[str, str]:
"""Return lossless text and Base64 representations for collected bytes."""
try:
return data.decode("utf-8"), ""
except UnicodeDecodeError:
return "", base64.b64encode(data).decode("ascii")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

问题: _encode_collected_content 以"能否通过严格 UTF-8 解码"作为二进制判定标准,把所有解码失败的内容(包括文本文件)路由进 content="" + content_base64,而下游 _should_inline_file_content 的新守卫(_skill_run.py:131)对任何设置了 content_base64 的文件直接返回 False,导致原本内联可见的文本内容从 skill_run 响应中消失。

触发条件: ① 文本 MIME 文件含非 UTF-8 字节(如 GBK/latin-1 编码的 .csv/.txtdetect_content_type 按扩展名返回 text/plain);② 超过 MAX_READ_SIZE_BYTES(4 MiB)的 UTF-8 文本文件被 fetcher 按字节截断(local read_bytes()[:max_bytes]、cube data[:max_bytes]、container f.read(max_bytes)),截断点落在一个多字节字符中间导致解码失败。

实际影响: 变更前这些文件的 content 为可读的部分文本(含 U+FFFD 替换符),模型可看到内容;变更后 skill_runoutput_files[].contentprimary_output 均为空,模型把有效文本文件误读为空文件,且 _filter_failed_empty_outputs(失败运行时 f.content 为空 + declarative 路径 size_bytes 恒为 0)会直接丢弃该文件并提示"empty outputs"。

修正方向: 不再以解码成败判定二进制:对解码失败的文本 MIME 文件保留 errors="replace" 的部分文本到 content 同时附加 content_base64 供无损恢复;或在 _should_inline_file_content 中当 content 为空但 content_base64 非空且 MIME 为文本类型时,改为允许内联 base64 解码后的内容。

Comment on lines +40 to +44
def get_bytes(self) -> bytes:
"""Return the collected content without losing binary bytes."""
if self.content_base64:
return base64.b64decode(self.content_base64, validate=True)
return self.content.encode("utf-8")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

问题: 新增的 get_bytes()CodeFileManifestFileRef 双份实现)使用 base64.b64decode(self.content_base64, validate=True):对任何缺 padding(如 'gA')或含非 base64 字母字符的输入抛出未捕获的 binascii.Error;且对 truncated=True 的文件静默返回被截断的字节前缀,无任何提示——ManifestFileRef 没有 truncated/size_bytes 字段,declarative 路径上 getattr(fr, "truncated", False) 恒为 False。

触发条件: CodeFile/ManifestFileRef 是公开导出的 SDK 类型(code_executors/__init__.py),外部调用方从协议数据/JSON 反序列化时构造了非规范 base64(如 JSON 序列化中 = 被转义、换行折叠、截断的流式 payload),或调用 get_bytes() 恢复一个被 4 MiB 上限截断的文件。

实际影响: 前者直接抛出 binascii.Error: Incorrect padding 使调用方崩溃(内部 b64encode 产物总是规范 padding,故仅外部构造可达);后者静默返回不完整字节,按 get_bytes() 重建的文件损坏且无法感知截断。

修正方向:content_base64 增加 pydantic 校验/规范化(或 get_bytes() 内捕获 binascii.Error 并转为可读错误),并在 ManifestFileRef 补齐 truncated/size_bytes 字段或在 get_bytes() 的文档/返回中显式暴露截断状态;建议两处 get_bytes() 抽为共享函数避免双份漂移。

Comment on lines +171 to +176
content, content_base64 = _encode_collected_content(data)
out.append(
CodeFile(
name=rel,
content=data.decode("utf-8", errors="replace"),
content=content,
content_base64=content_base64,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

问题: _build_code_files(collect() 路径)对每个非 UTF-8 文件无条件执行 base64 编码(_base_workspace_runtime.py:171),即使调用方从不读取内容(skill_run 的 _to_run_file 立即丢弃);而 _build_manifest_output:288)做到了仅在 spec.inline 时编码。同类逻辑却不对称。

触发条件: 任意产生二进制文件的 collect() 调用——默认最多 100 个文件、每个上限 4 MiB,单文件 base64 结果约 5.6 MiB 的 str 常驻 CodeFile 直至请求结束。

实际影响: 每个二进制文件多出约 1.33 倍字节的无效编码工作与峰值内存(编码期间同时持有原始 bytes 与 base64 str),在批量收集(如 auto-export 20 个文件)时放大为数百 MiB 的临时内存占用;同时 total_bytes 预算按原始字节统计,模型侧实际驻留的 base64 文本约超出 1.33 倍。

修正方向:_build_manifest_output 对齐:为 _build_code_files 增加惰性编码(如提供 content_base64: Optional[str] = None 由消费方按需解码),或在收集层不生成 base64、仅保留原始 bytes 的 get_bytes() 来源。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants