feat(run_python): 宿主语法预检——中文正文硬拼进源码的语法坏码提前拦下+锐化诊断(bump 0.58.34)

工具失败面板最大头(run_python/exit SyntaxError,20 条真实样本):模型手写
python-docx 生成 docx/报告时把中文正文内联进 T("中文…"),引号/标点崩坏 →
SyntaxError → 改 → 再崩,同一文件一分钟连撞 6 次烧 token。三类:
- A 引号提前闭合(最多):中文串里 ASCII 引号 " 提前闭合外层字符串,Python 却报
  "forgot a comma?"/"unterminated string" 主动误导,模型照着加逗号越修越错。
- B 全角标点漏进代码位(center=True、/采购():invalid character U+XXXX。
- C 括号/引号结构崩。

根因不是缺写长文工具(write 早能写长文进文件),而是 docx/报告无渲染管线——
documents skill 只做检索、不管 docx 生成,模型裸手撸 python-docx 内联中文;ppt 有
SVG→pptx 管线故崩得少。本轮先落机检止血(硬约束靠平台机检,不塞菜谱):

新增 core/pysyntax.py(不 import litellm,本机可测)precheck_python(code):host 上
compile()(host 3.12 ≡ sandbox python:3.12-slim,零版本偏差;只解析不执行、与依赖
无关),命中 SyntaxError 返锐化中文诊断(出错行+光标+按错误类型定向:invalid-char→
全角改半角、forgot-comma/unterminated+含中文→ASCII 引号提前闭合三改法、含中文统一
追加"正文别拼进 .py,write 进 .md/.txt 再 read"根治提示)。接进 run_python 所有派发
路径(inline/script_path/background × docker+host 两执行器,_prepare_script 内嵌覆盖
script_path 全路径 + inline 分支各加一处),命中即早返、跳过 docker 往返。

零误伤:只拦本就跑不了的码(compile 不过=运行必失败),通过原样放行、行为不变;
既有 30 个 executor/run_python 测试(合法码 fixture)全绿印证。白捡协同:预检把语法
失败从 [stderr]…[exit 1] 改成 [Error] 前缀+固定首行,loop._RepeatGuard 的 err-streak
(0.58.33)得以兜住"反复交语法坏码"的循环(旧 [exit 1] 形态它抓不到)。

测试 tests/test_pysyntax.py 11 用例(A/B/C + 合法中文串/中文引号不误伤)全绿。
下一步(未做,单列选型):docx 渲染管线,正文 write 进纯文本文件→渲染工具 read 生成
docx,正文永不进 Python 源码、也不走大正文当工具入参(避免触发 0.58.33 修的 wire
乱序畸形)。additive 内部机制,不碰对外 API/DB 契约。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
caoqianming 2026-07-17 08:55:55 +08:00
parent 3c60c5c736
commit 2a60243c02
6 changed files with 226 additions and 28 deletions

View File

@ -23,6 +23,7 @@
### 2026-07
- **07-17 / 0.58.34**:**run_python 语法预检:宿主 compile() 拦下中文正文硬拼进源码的语法坏码**(失败面板最大头,20 条真实样本实证:模型手写 python-docx 生成 docx/报告时把中文正文内联进 `T("中文…")`,引号/标点崩坏 → SyntaxError → 改 → 再崩,`gen_acceptance_report_v3.py` 一分钟内连撞 6 次)。三类:**A 引号提前闭合**(最多,中文串里用 ASCII 引号 `"` 把外层字符串提前闭合;Python 却报 `Perhaps you forgot a comma?`/`unterminated string`,**主动误导**模型照着加逗号越修越错)、**B 全角标点漏进代码位**(`center=True、`/`采购(`,`invalid character 'X'(U+XXXX)`)、**C 括号/引号结构崩**。**根因判断**:症结不是"缺写长文的工具"(write 早能写长文进文件),而是**docx/报告没有渲染管线**——documents skill 只做文献检索、不管 docx 生成,模型裸手撸 python-docx 内联中文;而 ppt skill 有 SVG→pptx 管线(正文进 SVG 文本节点),所以 ppt 崩得少。**本轮先落机检止血**(对标 [[feedback_skill_ban_no_recipe]] "硬约束靠平台机检"):新增独立模块 `core/pysyntax.py`(不 import litellm,`tests/test_pysyntax.py` 本机可跑,11 用例覆盖 A/B/C + 合法中文串/中文引号不误伤)`precheck_python(code)`——host 上 `compile()`(host 3.12 ≡ sandbox python:3.12-slim,零版本偏差;只解析不执行、与依赖无关),命中 SyntaxError 返**锐化中文诊断**(出错行+光标+按错误类型定向:invalid-character→全角改半角、forgot-comma/unterminated+含中文→ASCII 引号提前闭合的三改法、含中文统一追加"正文别拼进 .py,write 进 .md/.txt 再 read"根治提示)。接进 **run_python 所有派发路径**(inline/script_path/background × docker+host 两执行器,`_prepare_script` 内嵌覆盖 script_path 全路径 + inline 分支各加一处):命中即 `ToolResult(hint, exit_code=2)` 早返、**跳过 docker 往返**。**零误伤**:只拦本就跑不了的码(compile 不过=运行必失败),通过则原样放行、行为不变;既有 30 个 executor/run_python 测试(合法码 fixture)全绿印证。**白捡协同**:预检把 run_python 语法失败从 `[stderr]…[exit 1]` 改成 `[Error]` 前缀+固定首行,`loop._RepeatGuard` 的 err-streak(0.58.33 ②)得以兜住"反复交语法坏码"的循环(旧 `[exit 1]` 形态它按非错误放行、抓不到)——① 诊断准(修得快)+ ② 撞墙拦(停得住)合围。**下一步(未做,单列选型)**:docx 渲染管线(正文 write 进纯文本文件 → 渲染工具 read 生成 docx,正文永不进 Python 源码,**也不能走"大正文当工具入参"**——那会把长中文塞进 tool_call arguments,触发 0.58.33 修的 wire 乱序畸形);沙箱现有 python-docx+markitdown(docx→md 单向)、无 pandoc/libreoffice,自建渲染器 vs 装 pandoc 待拍板。additive 内部机制,不碰对外 API/DB 契约。相关 memory:skill 禁令不带配方 / deepseek 畸形 salvage 案。
- **07-17 / 0.58.33**:**堵工具失败聚集里两条反复烧 token 的裂缝**(扫近 14 天失败面板 top13 定位,`scripts/standalone` 复现口径)。**裂缝①「必填 key 被吞」畸形(面板 #3 `edit 缺少必填参数 ['path']`,14 次 / 13 task / 9 用户,最广)**:定层查一条原始 tool_call 坐实是**流式 wire 乱序**——某键的值碎片被瞬移拼进相邻字符串(实证:path 的 `.../gen_final_report_v2.py` 被吞进 `old_str` 尾部,独立的 `"path"` 键随之消失),跨 provider(`call_00_` 网关档 + `toolu_` Anthropic 系),非模型漏参。这类与既有 char-0 前缀畸形的关键区别是 **JSON parse 成功** → 既不命中 `_malformed_tool_calls`(只抓 parse 失败)、salvage 也救不了(parse-to-end/key 白名单对合法但错位无能),一路漏到 executor 才在语义层报错、回 `[Error]` 喂回模型 → 再拼再乱序反复烧。**修**:`_toolcalls_partial_args`(解析为非空 dict 且 `0<len(missing_required)<len(required)`,即至少一个必填在场、至少一个缺失=错位吞键特征;空 `{}`/必填全缺不算,仍交 executor+RepeatGuard)接进 `_stream_llm` 的 attempt 循环,和畸形/空响应同类:丢弃本轮走**非流式重试**(服务端一次拼好绕开流式 delta 乱序),耗尽仍缺才落回 executor 返「缺必填参数」(多为真漏参,不再空转);`_log_partial_args` 落 `usage_events`(kind=`tool_malformed`,error 签名固定 `missing required keys [...]`,和 char-0 型区分)——这类此前伪装成普通 messages `[Error]`、面板完全看不见,现纳入可观测。**裂缝②「换参数撞同一堵墙」(面板 #2 `edit old_str not found` 单 task 9 次 / 24h 7 次)**:模型每次微调 old_str 重试,精确 args 指纹每次不同 → `_RepeatGuard` 的 arg 判据不累计,只能等 `_STALL_LIMIT` 慢慢兜。**修**:`_RepeatGuard` 补第二道判据——按工具的**连续同类错误 streak**(`_norm_err` 抹平路径/数字后签名相同、跨 ≥2 个不同 args),SOFT 注入定向提示(先 read/grep 看确切内容再动手)、HARD 拦截一次并重置到 SOFT(非永久封死,换路后重试留活口);任一非错误结果立即清零该工具 streak。测试 `tests/test_loop_repeat_guard.py` +12(err-streak 5 + partial-args 7),全 23 相邻测试(salvage/toolfail_malformed)绿。**本机验证坑**:`core/loop.py` 顶层 `import litellm` 本机导入死卡(GitHub cost map),纯逻辑测试用 `sys.modules` 注入 litellm+子模块 stub 绕开跑真代码;失败面板扫描脚本因 import 链同样卡,改直连 psycopg 复刻 `scan_tool_failures` 口径查(结果与走 `core` 的交叉验证一致)。盲区:provider-wire 抖动本机复现不出、/verify 端到端跑不了,靠单测 + 生产灰度。additive 内部机制,不碰对外 API/DB 契约。剩 ①run_python 中文全角标点/未闭合字符串致 SyntaxError(面板最大头,产物质量,待单独立项)、③超时 19 次未动。相关 memory:deepseek 畸形 salvage 案 / 高轮数烧 token 三根因。
- **07-15 / 0.58.32**:**空响应防御:provider 吐空不再被当正常收尾静默 done**(复盘 task 2a1bc25d "ppt3":unifyllm 网关对某档 Claude 偶发把 tool_use 漏成正文 / 直接吐空,回来的轮 `tool_calls=[]` 且正文空,`loop.py` 的 `if not tool_calls: done` 当"模型答完"收尾——run_status=idle、无报错、无终态失败,表现为"自己中断",只能人肉挖 DB 才发现;复测该 bug 现已消失/瞬态,但"空 tool_calls 直接 done"这个失败模式本身太隐蔽,任何 provider 未来吐一次空都会静默卡死,故加防御)。**两层**(对称既有畸形 salvage 链):① 自动重试——`_is_empty_response`(tc 空且 content 去空白为空;纯 tool_call 轮不误判)接进 `_stream_llm` 的 attempt 循环,和畸形同类:丢弃本轮走非流式重试(多数瞬态重发一次即好,用户无感),`_log_empty_response` stdout+`usage_events`(kind=`empty_response`,cost 0)双写留痕、DB 失败静默不阻塞;② 重试耗尽仍空——`run()` 收尾点分空-空分支:不发静默 `done`,改发**可见 warn**「模型返回空响应(已自动重试仍为空),已停止,回复『继续』可重试」+ done(复用 stall 熔断的 warn+自停话术,run_status 落 idle 可续),**不引入终态 error**(空响应可恢复,红 error 过重且与既有路径不对称)。③ 可观测——`core/toolfail.py` 加第四段扫描 kind=empty_response 聚成 tool=`(empty)`/kind=`empty` 的 cluster(sample=model_profile 看哪个网关档在吐空),即便自动救回也留痕、跨 task 系统性吐空才冒头(单次瞬态 <阈值不触发)。**刻意不做**内容嗅探式"narrated tool_call 特征重试":marker 每次变且与助手正常解释/贴代码的正文高度重叠,假阳性(把合法回答判坏反复重试)比它防的罕见静默停更糟——那类只靠面板留痕兜不改热路径行为测试:`tests/test_loop_empty_response.py`(_is_empty_response 四态含纯 tool_call 不误判 + 重试恢复/耗尽返空/正常文本不重试)、`tests/test_toolfail_malformed.py` +2(空响应聚集 / 单次瞬态不触发),顺手补回 `test_loop_malformed_retry.py` salvage(0.58.24)落地后失修的 `loop.executor` ( schemas salvage False 走既有重试)。 247 测试绿盲区:provider-wire 抖动本机复现不出、/verify 端到端跑不了,靠单测+生产灰度additive 内部机制,不碰对外 API/DB 契约相关 memory:unifyllm tool_use 漏正文案 / deepseek 畸形 salvage
- **07-15 / 0.58.31**:**撤回 §8.11 delegate(0.58.30 曾整体落地,revert + force-push 抹除)**。delegate(隔离检索子循环:FilteredExecutor + 内存态子 Session + loop 拦截 + 降 flash)机制本身没错、对标 Claude Code subagent 也成立,但**触发信号没坐实就上了**:自评时回看 `diag_search_args.py`,motivating 案子 `document_search` 122 次呈"一批批不同材料体系并行搜"形态,更像**批量扇出而非结果驱动探索**——若属实其正解与 mp_search 同为**批量工具 `document_search_batch`**(便宜/可预测/可诊断),delegate 是过度设计;叠加子循环 transcript 不落盘(诊断驱动的功能反不可诊断)、20 轮上限对 122 次负载偏低、强制 flash 对难检索可能降质等未验证坑。故整体撤回,只留验证过的批量工具(0.58.28)。**重建前置收紧**:先用 diag 确认某检索 task 的 query 是真探索(依赖前序结果、无法一次列全)而非可枚举批量,是批量走批量工具、只有真探索才 delegate(DESIGN §8.11 记该教训)。撤法:`git reset --hard` 到 0.58.29 + `--force-with-lease` 推,delegate commit 从历史抹除(远程 tip 确认无他人后续提交才 force);保留本条 PROGRESS 记录"试过并撤回"免日后盲目重建。

View File

@ -1,3 +1,3 @@
# zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。
# 改版本只动这一行。
__version__ = "0.58.33"
__version__ = "0.58.34"

View File

@ -49,6 +49,7 @@ _CANCEL_POLL_INTERVAL_S = 0.2
from .executor import ExecCtx, Executor, ToolResult
from .executor_host import HostExecutor
from .pysyntax import precheck_python
from .sandbox import SandboxPool
from tools.base import compact_tool_output
@ -222,6 +223,9 @@ class DockerExecutor(Executor):
content="[Error] bad arguments to run_python: code or script_path must be provided",
exit_code=2,
)
hint = precheck_python(code)
if hint:
return ToolResult(content=hint, exit_code=2)
timeout = int(args.get("timeout") or 120)
# tmp .py 落 host 侧 `.zcbot_tmp/<task_id>/<rand>.py`;
@ -303,6 +307,9 @@ class DockerExecutor(Executor):
inner = f"python {container_script}"
display, kind = f"python {script_path.strip()}", "python"
elif isinstance(code, str) and code.strip():
hint = precheck_python(code)
if hint:
return ToolResult(content=hint, exit_code=2)
(d / "script.py").write_text(code, encoding="utf-8", newline="\n")
inner = f"python {cdir}/script.py"
display, kind = f"python <inline {len(code)} chars>", "python"
@ -396,17 +403,25 @@ class DockerExecutor(Executor):
host.resolve().relative_to(self.user_root)
except (ValueError, OSError):
return "", f"[Error] script_path out of bounds: {script_path}"
if host.is_file():
return container, None
if isinstance(code, str) and code.strip():
host.parent.mkdir(parents=True, exist_ok=True)
host.write_text(code, encoding="utf-8")
return container, None
return "", (
f"[Error] script_path not found: {script_path}"
"先用 write 创建该文件再执行,或直接用 code 传源码;"
"相对路径基于 task 工作目录,不要带工作目录名前缀。"
)
if not host.is_file():
if isinstance(code, str) and code.strip():
host.parent.mkdir(parents=True, exist_ok=True)
host.write_text(code, encoding="utf-8")
else:
return "", (
f"[Error] script_path not found: {script_path}"
"先用 write 创建该文件再执行,或直接用 code 传源码;"
"相对路径基于 task 工作目录,不要带工作目录名前缀。"
)
# 语法预检:host 侧 compile() 拦下语法坏码(中文正文硬拼进源码的引号/标点崩坏),
# 返锐化诊断、省一次进容器往返;通过则原样放行。读盘失败不拦(交容器报)。
try:
hint = precheck_python(host.read_text(encoding="utf-8", errors="replace"))
except OSError:
hint = None
if hint:
return "", hint
return container, None
# ── fs tools(read/write/edit/glob/grep)──────────────────

88
core/pysyntax.py Normal file
View File

@ -0,0 +1,88 @@
"""run_python 语法预检:宿主侧 compile() 拦下语法坏码,返回中文感知的锐化诊断。
背景(2026-07,工具失败面板最大头:run_python/exit SyntaxError,20 条真实样本):
模型手写把中文正文硬拼进 Python 源码生成 docx/pptx(python-docx `T("中文…")`),
引号/标点崩坏 SyntaxError 再崩,同一文件一分钟内连撞 6 次烧 token三类:
- A 引号提前闭合(最多):中文串里用了 ASCII 引号 `"` 把外层字符串提前闭合,
Python 却报 `Perhaps you forgot a comma?` / `unterminated string`(**主动误导**
模型照着加逗号越修越错)
- B 全角标点漏进代码位:`center=True``采购` `invalid character 'X' (U+XXXX)`
- C 括号/引号结构崩:mangled 内联命令未闭合三引号
预检零误伤:host 3.12 sandbox 3.12,compile() 只解析不执行(与依赖无关);compile
通过则原样放行行为不变,不通过则本就跑不了 提前返更准的话 + 省一次 docker 往返
另有协同:预检结果以 `[Error]` 前缀返回,run_python 语法失败从 `[stderr][exit 1]`
变成 `[Error]` 形态,loop._RepeatGuard err-streak 得以兜住反复交语法坏码的循环
(旧的 `[exit 1]` 形态它按非错误放行抓不到)
import litellm/任何重依赖 stdlib,`tests/test_pysyntax.py` 本机可跑
(loop.py 顶层 import litellm 在本机导入期卡死,守卫逻辑单列才测得了)
"""
from __future__ import annotations
from typing import Optional
def _has_cjk(s: str) -> bool:
return any("" <= c <= "鿿" for c in s)
def precheck_python(code: Optional[str]) -> Optional[str]:
"""对 Python 源码做语法预检。通过返回 None;不通过返回锐化的中文诊断([Error] 前缀)。"""
if not isinstance(code, str) or not code.strip():
return None # 空/非字符串交给现有 bad-arguments 分支,不在此判
try:
compile(code, "<script>", "exec")
return None
except SyntaxError as e:
return _format(code, e)
except (ValueError, TypeError):
# 含 null 字节等 compile 会抛 ValueError —— 不是语法误导型,交给运行期报错
return None
def _format(code: str, e: SyntaxError) -> str:
lines = code.splitlines()
lineno = e.lineno or 0
msg = e.msg or "语法错误"
src = lines[lineno - 1] if 1 <= lineno <= len(lines) else ""
src = src.rstrip()
out = [
"[Error] Python 语法预检未通过(未进沙箱执行,先在宿主解析拦下):",
f" SyntaxError: {msg}(第 {lineno} 行)",
]
if src:
shown = src[:200]
out.append(" " + shown)
off = (e.offset or 0) - 1
if 0 <= off < len(shown):
out.append(" " + " " * off + "^")
low = msg.lower()
has_cjk = _has_cjk(src)
tips = []
if "invalid character" in low:
# Python 已点名具体字符 + 码位,补一句半角/引号内的判断
tips.append(
"报错处是全角/中文标点。若它在代码位置(用了全角 ,。、;:()等)请改半角;"
"若它本属正文,请确认整段正文在字符串引号内。"
)
elif has_cjk and any(
k in low for k in (
"forgot a comma", "unterminated string", "unmatched",
"invalid syntax", "was never closed", "closing parenthesis",
)
):
tips.append(
"疑似中文正文里的 ASCII 引号 \"' 提前闭合了字符串。改法任选:"
"① 整段中文用三引号 '''''' 包裹;② 正文里的引号写成中文引号 “”/;③ 用 \\\" 转义。"
)
if has_cjk:
tips.append(
"根治:大段中文正文别硬拼进 .py —— 用 write 写进 .md/.txt,脚本里 read 后再灌进 "
"docx/pptx,正文就不经过 Python 语法/引号。"
)
if tips:
out.append(" 修法:" + " ".join(tips))
return "\n".join(out)

80
tests/test_pysyntax.py Normal file
View File

@ -0,0 +1,80 @@
from __future__ import annotations
import sys
import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from core.pysyntax import precheck_python # noqa: E402
class TestPrecheckPython(unittest.TestCase):
# ── 放行:合法码不误伤 ──
def test_clean_code_passes(self):
self.assertIsNone(precheck_python("import os\nx = 1 + 2\nprint(x)\n"))
def test_valid_chinese_in_string_passes(self):
"""中文正文在正确引号里 = 合法,绝不误报。"""
self.assertIsNone(precheck_python('x = "中文正文,含全角标点。没问题!"\n'))
def test_chinese_quotes_in_string_passes(self):
"""中文引号 “” 只是普通字符,不是分隔符,合法。"""
self.assertIsNone(precheck_python('x = "他说“你好”,然后走了。"\n'))
def test_empty_and_none_pass(self):
self.assertIsNone(precheck_python(None))
self.assertIsNone(precheck_python(""))
self.assertIsNone(precheck_python(" \n "))
self.assertIsNone(precheck_python(123)) # 非字符串
# ── A 类:中文串里 ASCII 引号提前闭合(样本 #1/#8/#16) ──
def test_ascii_quote_in_chinese_string(self):
code = 'T("5产学研合作形成"产学研用"一体化链条。")\n'
hint = precheck_python(code)
self.assertIsNotNone(hint)
self.assertTrue(hint.startswith("[Error]"))
self.assertIn("第 1 行", hint)
self.assertIn("引号", hint) # 定向引号诊断
self.assertIn("write", hint) # 根治提示(正文进文件)
def test_unterminated_string_with_cjk(self):
code = "set_cell(c, '国际一流双碳大数据平台], '负责人')\n"
hint = precheck_python(code)
self.assertIsNotNone(hint)
self.assertIn("引号", hint)
# ── B 类:全角标点漏进代码位(样本 #7/#9) ──
def test_fullwidth_punct_in_code(self):
code = 'add_h("2025年12月", 14, center=True、name)\n'
hint = precheck_python(code)
self.assertIsNotNone(hint)
self.assertIn("半角", hint) # 全角→半角定向诊断
def test_fullwidth_paren_in_code(self):
code = 'set_cell(tbl, "响应函", "第五章(附件)中的要求。", "通过")\n\n'
hint = precheck_python(code)
self.assertIsNotNone(hint)
# ── C 类:结构崩坏 ──
def test_unmatched_bracket(self):
hint = precheck_python("for i in enumerate(doc.paragraphs[:5()[:80]}'\")\n")
self.assertIsNotNone(hint)
self.assertTrue(hint.startswith("[Error]"))
# ── 非中文语法错:仍拦,但不追加中文根治提示 ──
def test_non_cjk_syntax_error(self):
hint = precheck_python("def f(:\n pass\n")
self.assertIsNotNone(hint)
self.assertTrue(hint.startswith("[Error]"))
self.assertNotIn("write 写进", hint) # 无中文正文,不追加"别拼中文进 py"
def test_lineno_reported(self):
code = "a = 1\nb = 2\nc = @\n"
hint = precheck_python(code)
self.assertIsNotNone(hint)
self.assertIn("第 3 行", hint)
if __name__ == "__main__":
unittest.main()

View File

@ -16,6 +16,7 @@ from pathlib import Path
from typing import Optional
from core import procs
from core.pysyntax import precheck_python
from .base import Tool, compact_tool_output
@ -87,22 +88,29 @@ class RunPythonTool(Tool):
alt = self.user_root / script_path.replace("\\", "/").strip().lstrip("./")
if alt.is_file():
script = alt
if script.is_file():
return script, None
if isinstance(code, str) and code.strip():
if self.user_root is not None:
try:
script.resolve().relative_to(self.user_root.resolve())
except (ValueError, OSError):
return None, f"[Error] script_path out of bounds: {script_path}"
script.parent.mkdir(parents=True, exist_ok=True)
script.write_text(code, encoding="utf-8")
return script, None
return None, (
f"[Error] script_path not found: {self._display(script)}"
"先用 write 创建该文件再执行,或直接用 code 传源码;"
"相对路径基于 task 工作目录,不要带工作目录名前缀。"
)
if not script.is_file():
if isinstance(code, str) and code.strip():
if self.user_root is not None:
try:
script.resolve().relative_to(self.user_root.resolve())
except (ValueError, OSError):
return None, f"[Error] script_path out of bounds: {script_path}"
script.parent.mkdir(parents=True, exist_ok=True)
script.write_text(code, encoding="utf-8")
else:
return None, (
f"[Error] script_path not found: {self._display(script)}"
"先用 write 创建该文件再执行,或直接用 code 传源码;"
"相对路径基于 task 工作目录,不要带工作目录名前缀。"
)
# 语法预检(与 DockerExecutor 同):compile() 拦下语法坏码,返锐化诊断。
try:
hint = precheck_python(script.read_text(encoding="utf-8", errors="replace"))
except OSError:
hint = None
if hint:
return None, hint
return script, None
def _filtered_env(self) -> dict:
env = os.environ.copy()
@ -137,6 +145,9 @@ class RunPythonTool(Tool):
)
shown = self._display(script)
elif isinstance(code, str) and code.strip():
hint = precheck_python(code)
if hint:
return hint
proc_id, _ = procs.launch_host(
anchor, self.task_id,
kind="python",
@ -168,6 +179,9 @@ class RunPythonTool(Tool):
if err is not None:
return err
elif isinstance(code, str):
hint = precheck_python(code)
if hint:
return hint
# 写到临时文件,避免 -c 转义问题
with tempfile.NamedTemporaryFile(
suffix=".py", mode="w", delete=False, encoding="utf-8"