DEBUG.WIKI
野草云;香港 200Mbps 峰值;不限月流量 · BGP VPS;查看套餐 →
雨云;给项目一台云服务器
晚安云;香港 CN2 ¥15/月

prompt-toolkit 保存历史报 surrogates not allowed,先检查代理码点

FileHistory 旧版严格 UTF-8 编码会拒绝代理码点;3.0.53 改为替代保存。命令需要精确重放时,应在 append_string 前校验,避免把不报错误当成字符完整。

交互输入包含剪贴板带来的代理码点时,prompt-toolkit 旧版可能在保存历史记录时抛出:

UnicodeEncodeError: 'utf-8' codec can't encode character '\udead': surrogates not allowed

正常的 emoji 字符与 Python 字符串中残留的 UTF-16 代理码点不是一回事。"🐺" 是可编码的 Unicode 字符;"\ud83d\udc3a" 则包含两个代理码点,不能直接按严格 UTF-8 编码。单独的 "\udead" 也没有对应的完整字符。

prompt-toolkit 3.0.52 的 FileHistory.store_string() 直接进行严格 UTF-8 编码。3.0.53 已改用 errors="replace" 避免这项保存异常,但代理码点会变成替代内容,不能把“不再崩溃”等同于原字符串完整保存。3.0.53 要求 Python 3.10 及以上。

在交给历史记录之前决定输入策略

命令历史需要准确重放时,优先在输入边界拒绝不能按严格 UTF-8 编码的字符串,并让用户重新输入或修正剪贴板来源。应在 append_string() 之前检查,避免历史对象的内存状态和文件状态已经发生部分变化:

from prompt_toolkit.history import FileHistory


history = FileHistory("command-history.txt")


def save_exact_history(text: str) -> None:
    try:
        text.encode("utf-8", errors="strict")
    except UnicodeEncodeError as exc:
        raise ValueError("输入含不完整的 Unicode 代理码点,未保存历史") from exc
    history.append_string(text)

这段处理不会自动修复文本,也不应在捕获 ValueError 后继续执行原命令。交互程序需要把提示反馈给用户,再按自身规则决定是否重新编辑。若应用由 PromptSession 自动保存历史,应把校验接入提交前的输入处理,不能仅给旁边一个未被调用的函数加检查。

确认是 UTF-16 单元串后,才做配对解码

有些接口错误地把 UTF-16 的两个代码单元直接塞进 Python str。只有明确知道输入来自这种接口,才能按该接口约定组合成真正的 Unicode 字符;未配对的代理仍应拒绝。任意字符串都做这种转换,会掩盖来源错误,也无法给孤立代理补出丢失的另一半。

errors="ignore" 会删字符,errors="replace" 会改字符,surrogatepass 则会产生普通 UTF-8 读取器不能按标准字符接受的字节。它们各有数据格式前提,不能互换着用来“保留所有内容”。3.0.53 的替代策略适合允许历史文字有所损失的场景;需要精确命令或标识符时,仍应保留上述校验。

旧版写入失败可能已经留下该条记录的时间头或前几行。先保留历史文件副本再检查最后一条,不要只因出现编码错误就清空整个历史。验证应覆盖普通中文、实际 emoji、配对代理形式和孤立代理;需要精确保存的输入,重新加载后的文本必须与提交值一致,而不仅仅是没有异常。