diff --git a/PROGRESS.md b/PROGRESS.md index d54ff1f..1a9aa92 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -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/.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 ", "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)────────────────── diff --git a/core/pysyntax.py b/core/pysyntax.py new file mode 100644 index 0000000..d52c9a5 --- /dev/null +++ b/core/pysyntax.py @@ -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, "