zcbot/core/pysyntax.py

89 lines
3.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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)