89 lines
3.9 KiB
Python
89 lines
3.9 KiB
Python
"""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)
|