上一篇我整理了《每天批量处理 20 篇 WordPress 历史文章:代码块迁移、摘要补全与英文覆盖翻译完整 SOP》。
那一阶段主要处理已经属于 Gutenberg 格式、但正文中仍然存在 SyntaxHighlighter 的历史文章。继续清理以后,我发现站内仍然有不少中文历史文章包含 SyntaxHighlighter;进一步检查后确认,它们并不是此前漏扫,而是属于另一类历史格式:正文中同时存在 Gutenberg 区块、Classic / 经典编辑器内容和 SyntaxHighlighter,分析器会将其识别为 editor_format=mixed。
此前 Gutenberg + SyntaxHighlighter 阶段只处理 editor_format=gutenberg 的文章,因此这些 Mixed 文章自然被留到了下一阶段。历史快照中共有 717 篇 Mixed 候选,其中 712 篇的主要问题是 editor-format-mixed;另外 5 篇还同时存在其他代码格式或结构异常,暂时单独保留。
因此第二阶段的目标很明确:先把 Classic / Mixed 内容规范化为 Gutenberg,再把 SyntaxHighlighter 迁移为 Code Block Pro,完成生产只读验证后,由仓库自动生成中文摘要并覆盖英文译文。
一、Mixed 阶段为什么要单独处理
Mixed 文章和前一阶段最大的区别,不是 SyntaxHighlighter 本身,而是整篇文章还没有真正成为规范 Gutenberg。即使后台已经能看到部分 Gutenberg 区块,也可能仍有 Classic 内容游离在区块结构之外。
因此 Mixed 阶段不能只做“SyntaxHighlighter → Code Block Pro”。在进入自动摘要和英文覆盖翻译以前,还必须同时满足:Classic / Mixed 内容已经完成 Gutenberg normalization、自动转换后的排版已经人工检查、SyntaxHighlighter 已归零、Code Block Pro 数量和语言正确,并且生产只读验证确认 editor_format=gutenberg、classic_outside_blocks=false。
换句话说,“Classic 区块已经消失”并不能直接等价于“Gutenberg normalization 已经完成”。复杂 Classic 一键转换以后,仍然可能出现段落合并、标题变普通段落、编号错乱、换行消失、图片或 caption 位置变化等情况,这部分仍然需要人工检查。
二、两个仓库与人工职责
1. WordPress 翻译管线
仓库路径为 /home/wangqiang/code/wordpress-ai-translation-pipeline,主要负责 SlyTranslate 与 GLM 翻译定制、Gutenberg 结构保护、HTML / 代码 / 短代码保护、占位符校验以及英文整篇覆盖翻译。
2. 历史文章迁移仓库
仓库路径为 /home/wangqiang/code/wordpress-ai-excerpt-backfill,主要负责候选筛选、固定批次、人工转换状态、生产只读验证、中文摘要生成、英文覆盖翻译调度、执行证据、失败恢复与批次汇总。
3. 人工真正需要做什么
- 打开仓库指定的文章,先判断当前正文是否已经是正常 Gutenberg;
- 如果仍是 Classic、Mixed 或 unknown,再将正文规范化为 Gutenberg,并检查转换后的段落、标题、编号、列表和换行;
- 如果仍存在 SyntaxHighlighter,则迁移为 Code Block Pro,并逐块核对语言;如果本来就没有旧代码结构,不为了流程额外修改正文;
- 检查图片、caption、链接、正文顺序和是否存在损坏区块;
- 确认当前文章已经满足生产验证要求,最后批量记录人工检查或转换已经完成。
正常批次中,人工不需要自己寻找下一批文章,也不需要手工生成中文摘要或逐篇点击英文覆盖翻译。只有自动模型因内容安全过滤等确定性原因无法完成、且统一恢复已经不能通过正常自动路径安全推进时,才进入后文的 ChatGPT 人工兜底流程。
三、完整状态流转
正常主流程仍然沿用上一阶段的状态机:
awaiting_manual_conversion
→ mark-converted
→ awaiting_readonly_validation
→ validate-live
→ ready_for_execution
→ run-ready --execute
→ completed
Mixed 阶段真正新增的是“整篇 Gutenberg normalization 已完成”的人工确认,以及生产验证对 editor_format=gutenberg、classic_outside_blocks=false 的检查。
到剩余空摘要文章阶段以后,这套状态机继续保持不变。变化的是候选入口已经放宽:文章不必先属于 Mixed 或包含 SyntaxHighlighter;但进入 ready_for_execution 以前,仍然必须通过原有 Gutenberg、Classic 残留、SyntaxHighlighter、代码结构和生产安全验证。
生产验证阶段仍然需要区分 awaiting_readonly_validation 与 validation_failed;正式执行阶段的单篇失败则统一交给 recover。程序会根据本地状态、execution evidence、pre-write 基线和生产中文源自动选择安全恢复策略,后文单独整理。
四、每天开始前先检查仓库状态
每天开始操作以前,先进入仓库并检查全局状态:
cd ~/code/wordpress-ai-excerpt-backfill
python3 bin/history-migration.py status
python3 bin/history-migration.py summary
重点看“最新未完成批次”“下一步”“建议创建下一批”。如果仍然存在最新未完成批次,就继续完成当前批次,不要提前创建新的 20 篇。只有看到“最新未完成批次: 无”“建议创建下一批: True”“建议: all batches complete”时,才创建下一批。
五、创建新的 20 篇固定批次
2026 年 8 月 12 日:候选入口扩展到全部剩余空摘要文章
最初的 Mixed + SyntaxHighlighter 阶段使用 bin/build-mixed-syntaxhighlighter-batch.py 创建固定批次。到 2026 年 8 月 12 日,这一阶段以及最后 5 篇异常文章已经全部完成,历史迁移状态达到 45 个固定批次、857 篇文章全部 completed。
随后重新只读盘点生产环境,确认仍有 424 篇中文已发布历史文章满足“中文摘要为空、存在 Polylang 英文对应文章、英文文章为 publish、尚未进入现有固定批次”。因此后续阶段不再以 Mixed、SyntaxHighlighter 或某一种代码格式作为候选入口,而是继续沿用现有批次生成器,只把候选条件放宽到全部剩余空摘要历史文章。
批次排序规则不变,仍然固定按 published_at DESC、chinese_post_id DESC 排序,也就是从发布时间较新的历史文章向较旧文章推进;每批最多 20 篇,最后不足 20 篇时直接处理全部剩余候选。
脚本和批次名称目前仍然保留 mixed-syntaxhighlighter,这是为了继续兼容已经稳定运行的固定批次、状态目录和 history-migration 工作流,并不表示新阶段的候选仍然必须包含 SyntaxHighlighter。
确认上一批已经完成以后,可以执行:
cd ~/code/wordpress-ai-excerpt-backfill
summary_output="$(
python3 bin/history-migration.py summary
)"
printf '%s\n' "$summary_output"
if ! grep -q '^建议创建下一批: True$' <<<"$summary_output"; then
echo
echo "当前不允许创建下一批,请先完成最新未完成批次。"
else
date_tag="$(date +%Y%m%d)"
batch_id=""
batch_file=""
for number in $(seq -w 1 99)
do
candidate_id="mixed-syntaxhighlighter-${date_tag}-${number}"
candidate_file="data/analysis/mixed-syntaxhighlighter-migration-batch-${date_tag}-${number}.csv"
candidate_state_dir="data/state/history-migration/${candidate_id}"
if [ ! -e "$candidate_file" ] && [ ! -d "$candidate_state_dir" ]; then
batch_id="$candidate_id"
batch_file="$candidate_file"
break
fi
done
if [ -z "$batch_id" ]; then
echo "无法生成可用的新批次编号。"
else
raw_file="data/raw/history-general-candidates-20260812-01.jsonl"
preview_file="data/analysis/history-general-candidates-20260812-01-preview.csv"
translations_file="data/raw/history-general-candidates-20260812-01-translations.jsonl"
if [ ! -f "$raw_file" ]; then
echo "找不到原始候选快照:$raw_file"
elif [ ! -f "$preview_file" ]; then
echo "找不到 preview:$preview_file"
elif [ ! -f "$translations_file" ]; then
echo "找不到翻译关系:$translations_file"
else
echo
echo "新批次 ID:$batch_id"
echo "新批次文件:$batch_file"
if python3 bin/build-mixed-syntaxhighlighter-batch.py \
--preview "$preview_file" \
--translations "$translations_file" \
--output "$batch_file" \
--batch-id "$batch_id" \
--maximum 20 \
"$raw_file"
then
if [ -s "$batch_file" ] && [ "$(wc -l < "$batch_file")" -gt 1 ]; then
python3 bin/history-migration.py init-state --apply \
&& echo \
&& echo "新批次已经创建:" \
&& echo "batch_id=\"$batch_id\"" \
&& echo "csv_file=\"$batch_file\""
else
echo
echo "没有创建新批次。"
echo "请检查上面的 selected_count 和 remaining_eligible_count。"
fi
else
echo
echo "创建新批次失败,未执行 init-state。"
fi
fi
fi
fi
这里仍然先检查 summary 是否允许创建下一批;只有批次 CSV 确实生成且至少包含一条候选记录时,才执行 init-state --apply 并输出“新批次已经创建”。这样可以避免候选池已经耗尽时仍然误报创建成功。
如果最后剩余候选不足 20 篇,则直接处理全部剩余候选。
六、设置批次变量并输出后台编辑地址
每天只需要单独设置两个会变化的变量,例如:
batch_id="mixed-syntaxhighlighter-YYYYMMDD-01"
csv_file="data/analysis/mixed-syntaxhighlighter-migration-batch-YYYYMMDD-01.csv"
然后一次性输出当前批次仍未完成文章的后台编辑地址:
python3 - "$batch_id" "$csv_file" <<'PY'
import csv
import json
import sys
from pathlib import Path
batch_id = sys.argv[1]
csv_path = Path(sys.argv[2])
state_dir = Path("data/state/history-migration") / batch_id
with csv_path.open(encoding="utf-8-sig", newline="") as f:
rows = list(csv.DictReader(f))
items = []
for row in rows:
post_id = int(row["chinese_post_id"])
state_path = state_dir / f"chinese-{post_id}.json"
state = {}
if state_path.is_file():
state = json.loads(state_path.read_text(encoding="utf-8"))
status = state.get("workflow_status", "uninitialized")
if status == "completed":
continue
edit_url = (
"https://admin.shuijingwanwq.com/wp-admin/"
f"post.php?post={post_id}&action=edit"
)
items.append({
"post_id": post_id,
"english_id": row["english_post_id"],
"title": row["chinese_title"],
"syntax": row["before_syntaxhighlighter_count"],
"status": status,
"edit_url": edit_url,
})
print(f"批次:{batch_id}")
print(f"当前待处理:{len(items)}")
print()
for index, item in enumerate(items, 1):
print(
f"{index:02d}. "
f"zh={item['post_id']} "
f"en={item['english_id']} "
f"SH={item['syntax']} "
f"status={item['status']}"
)
print(f" 标题:{item['title']}")
print(f" 编辑:{item['edit_url']}")
print()
PY
七、人工检查并规范化为 Gutenberg
1. 先判断文章是否真的需要修改
进入剩余空摘要文章阶段以后,并不是每篇文章都必须修改正文。2026 年 8 月 12 日的只读盘点显示,424 篇正式候选中有 74 篇已经是正常 Gutenberg,并且没有需要迁移的旧代码格式;这类文章只需要人工确认结构和排版正常,不需要为了迁移流程再次保存或改写正文。
2. Classic / Mixed / unknown 规范化为 Gutenberg
如果文章仍然属于 Classic、Mixed 或 unknown,则继续按照此前规则规范化为 Gutenberg。简单 Classic 内容可以直接使用 WordPress 的“转换为区块”;复杂内容转换以后仍然必须人工检查排版。
保存以前至少确认:段落、标题、编号、列表、换行、图片、caption、链接和正文顺序正常;没有内容遗漏、重复或损坏区块。最终必须能够通过现有生产验证对 editor_format=gutenberg、classic_outside_blocks=false 等条件的检查。
3. 旧代码结构仍按现有规则处理
如果发现旧代码结构,则继续按照现有验证规则处理。SyntaxHighlighter 最终必须为 0;如果仍然存在 SyntaxHighlighter,则迁移为 Code Block Pro,并逐块核对语言。原区块已经声明语言时继续使用对应语言;没有声明语言时优先使用 Plaintext,不要让 Code Block Pro 无意继承上一个代码块的语言。
现有验证器禁止的 core/code、classic pre/code 或其他异常结构,也必须在进入生产只读验证以前完成规范化。当前剩余 424 篇候选的只读盘点中,SyntaxHighlighter 数量已经为 0;保留这项硬性验证,是为了确保全部历史文章处理结束以后可以安全删除 SyntaxHighlighter 插件。
这里不要手工生成摘要,也不要修改英文译文。
八、批量记录人工转换完成
全部文章完成必要的人工检查或转换以后,需要把状态从 awaiting_manual_conversion 推进到 awaiting_readonly_validation。对于本来已经是干净 Gutenberg 的文章,这里的确认表示已经人工检查,不代表正文必须发生修改。先确认 $batch_id 和 $csv_file,再执行:
read -r -p \
"确认当前批次文章均已完成 Gutenberg 状态检查;需要转换的文章已完成规范化,SyntaxHighlighter 已归零,如存在 Code Block Pro 也已完成语言核对。输入 YES 继续:" \
answer
if [ "$answer" != "YES" ]; then
echo "未确认,已停止,没有修改状态。"
else
python3 - "$batch_id" "$csv_file" <<'PY'
import csv
import json
import subprocess
import sys
from pathlib import Path
batch_id = sys.argv[1]
csv_path = Path(sys.argv[2])
state_dir = Path("data/state/history-migration") / batch_id
with csv_path.open(encoding="utf-8-sig", newline="") as f:
rows = list(csv.DictReader(f))
def status_of(post_id):
path = state_dir / f"chinese-{post_id}.json"
if not path.is_file():
raise RuntimeError(f"缺少状态文件:{path}")
return json.loads(path.read_text(encoding="utf-8"))["workflow_status"]
targets = [
row for row in rows
if status_of(int(row["chinese_post_id"]))
== "awaiting_manual_conversion"
]
print(f"批次:{batch_id}")
print(f"等待记录人工转换:{len(targets)}")
print()
success = []
failed = []
for index, row in enumerate(targets, 1):
post_id = row["chinese_post_id"]
syntax_before = row["before_syntaxhighlighter_count"]
cbp_after = row["expected_code_block_pro_count_after"]
result = subprocess.run(
[
sys.executable,
"bin/history-migration.py",
"mark-converted",
"--post-id", post_id,
"--syntax-count-before", syntax_before,
"--cbp-count-after", cbp_after,
"--language-review-confirmed",
"--gutenberg-normalization-confirmed",
],
text=True,
capture_output=True,
check=False,
)
final_status = status_of(int(post_id))
if result.returncode == 0 and final_status == "awaiting_readonly_validation":
success.append(post_id)
print(f"[{index}/{len(targets)}] 已记录:zh={post_id}")
else:
failed.append(post_id)
output = (result.stderr or result.stdout or "").strip().splitlines()
error = output[-1] if output else "没有错误摘要"
print(
f"[{index}/{len(targets)}] "
f"记录失败:zh={post_id} "
f"error={error}"
)
print()
print("========== 人工转换记录汇总 ==========")
print(f"待记录:{len(targets)}")
print(f"成功:{len(success)}")
print(f"失败:{len(failed)}")
if failed:
print("失败文章:" + ", ".join(failed))
raise SystemExit(1)
PY
fi
正常情况下,汇总应显示本批待记录文章全部成功,失败为 0。
九、执行生产只读验证
1. 从浏览器获取 Cookie 和 REST Nonce
生产只读验证需要使用当前已经登录 WordPress 后台的会话。WP_ADMIN_COOKIE 保存后台请求中的完整 Cookie 值,WP_REST_NONCE 保存同一登录会话中的 X-WP-Nonce 值。
可以在浏览器中按下面的方式获取:
- 登录当前使用的 WordPress 管理后台,并打开任意文章编辑页面;
- 打开浏览器开发者工具,切换到“网络 / Network”;
- 刷新编辑页面,或者执行一次不会破坏内容的后台操作;
- 在网络请求中筛选
wp-json,打开一个状态为 200 的 WordPress REST API 请求; - 在请求头中复制
Cookie的完整值,作为WP_ADMIN_COOKIE; - 在同一请求的请求头中复制
X-WP-Nonce的值,作为WP_REST_NONCE。
复制时只取请求头冒号后面的值,不要把 Cookie: 或 X-WP-Nonce: 这两个请求头名称一起复制。两项内容最好来自同一个成功请求,这样可以避免 Cookie 和 Nonce 分别属于不同登录会话。
Cookie 和 REST Nonce 都属于敏感认证信息,不应写入博客、截图、Git 仓库、脚本源码或提交记录。
2. 在当前终端设置环境变量
不要直接执行带有真实值的 export WP_ADMIN_COOKIE='...',否则内容可能进入 shell 历史。可以使用静默输入;粘贴时终端不会显示实际内容:
if [ -z "${WP_ADMIN_COOKIE-}" ]; then
read -r -s -p "请输入新的 WordPress Cookie:" WP_ADMIN_COOKIE
echo
export WP_ADMIN_COOKIE
fi
if [ -z "${WP_REST_NONCE-}" ]; then
read -r -s -p "请输入新的 X-WP-Nonce:" WP_REST_NONCE
echo
export WP_REST_NONCE
fi
这里的 export 只会让变量在当前 shell / 终端会话及其子进程中生效。关闭终端、重新打开新的 VS Code Terminal,或者换到另一个终端标签页以后,通常都需要重新设置。当前流程没有把这些敏感值写入 .env、~/.bashrc 或其他持久化文件。
3. 检查变量,并识别已经过期的认证信息
设置后可以只检查变量是否非空,不打印真实内容:
for name in WP_ADMIN_COOKIE WP_REST_NONCE
do
if [ -z "${!name-}" ]; then
echo "缺少环境变量:$name"
else
echo "已设置:$name"
fi
done
需要特别注意:这里的“已设置”只代表变量非空,并不代表 Cookie 或 REST Nonce 仍然有效。2026 年 7 月 31 日的实际执行中,我就遇到过 WP_REST_NONCE 已存在、但实际请求连续返回 HTTP 403 的情况;重新获取并设置新的 Nonce 后,同一批次立即恢复正常。
因此,如果多篇文章都在同一个 REST 预检位置连续返回 403,不应该优先怀疑文章内容,而应该先检查当前认证信息,尤其是 WP_REST_NONCE。需要更新时可以直接重新静默输入并覆盖旧值:
read -r -s -p "请输入新的 WordPress Cookie:" WP_ADMIN_COOKIE
echo
export WP_ADMIN_COOKIE
read -r -s -p "请输入新的 X-WP-Nonce:" WP_REST_NONCE
echo
export WP_REST_NONCE
如果已经确认 Cookie 仍然有效,也可以只重新设置 WP_REST_NONCE;无法确定时,最好从同一个新的成功 REST 请求中重新复制两项值。
4. 批量生产只读验证,并对偶发 SSH 超时有限重试
进入剩余空摘要文章阶段以后,生产只读验证继续沿用原有结构和生产安全检查。正式执行以前,当前中文正文必须已经成为正常 Gutenberg,classic_outside_blocks=false,SyntaxHighlighter=0,并通过现有 Code Block Pro、损坏区块、Polylang、发布状态、SHA-256 和 target drift 等检查。候选入口虽然已经放宽,但执行验证标准并没有放宽。实际运行中如果出现 batch read-only SSH query timed out 或 SSH exit 255,而状态仍然停留在 awaiting_readonly_validation,通常属于只读查询没有成功完成,可以有限重试。
missing=0
for name in WP_ADMIN_COOKIE WP_REST_NONCE
do
if [ -z "${!name-}" ]; then
echo "缺少环境变量:$name"
missing=1
fi
done
if [ "$missing" -ne 0 ]; then
echo "已停止,未执行生产只读验证。"
else
python3 - "$batch_id" "$csv_file" <<'PY'
import csv
import json
import subprocess
import sys
import time
from pathlib import Path
batch_id = sys.argv[1]
csv_path = Path(sys.argv[2])
state_dir = Path("data/state/history-migration") / batch_id
with csv_path.open(encoding="utf-8-sig", newline="") as f:
rows = list(csv.DictReader(f))
def status_of(post_id):
path = state_dir / f"chinese-{post_id}.json"
if not path.is_file():
raise RuntimeError(f"缺少状态文件:{path}")
return json.loads(
path.read_text(encoding="utf-8")
)["workflow_status"]
targets = []
unexpected = []
for row in rows:
post_id = int(row["chinese_post_id"])
status = status_of(post_id)
if status == "awaiting_readonly_validation":
targets.append(post_id)
elif status not in ("ready_for_execution", "completed"):
unexpected.append((post_id, status))
print(f"批次:{batch_id}")
print(f"本次待验证:{len(targets)}")
print()
success = []
failed = []
for index, post_id in enumerate(targets, 1):
passed = False
for attempt in range(1, 4):
result = subprocess.run(
[
sys.executable,
"bin/history-migration.py",
"validate-live",
"--post-id",
str(post_id),
],
text=True,
capture_output=True,
check=False,
)
final_status = status_of(post_id)
if result.returncode == 0 and final_status == "ready_for_execution":
success.append(post_id)
if attempt == 1:
print(f"[{index}/{len(targets)}] 验证通过:zh={post_id}")
else:
print(
f"[{index}/{len(targets)}] "
f"重试通过:zh={post_id} attempts={attempt}"
)
passed = True
break
output = (result.stderr or result.stdout or "").strip().splitlines()
error = output[-1] if output else "没有错误摘要"
print(
f"[{index}/{len(targets)}] "
f"第 {attempt}/3 次失败:zh={post_id} "
f"status={final_status} "
f"error={error}"
)
if final_status != "awaiting_readonly_validation":
break
if attempt < 3:
time.sleep(5)
if not passed:
failed.append(post_id)
print()
print("========== 只读验证最终汇总 ==========")
print(f"本次待验证:{len(targets)}")
print(f"本次通过:{len(success)}")
print(f"本次失败:{len(failed)}")
if unexpected:
print(
"非预期状态:"
+ ", ".join(
f"{post_id}:{status}"
for post_id, status in unexpected
)
)
if failed:
print("验证失败文章:" + ", ".join(map(str, failed)))
if failed or unexpected:
raise SystemExit(1)
PY
result=$?
echo
if [ "$result" -eq 0 ]; then
echo "========== 验证后的待执行文章 =========="
python3 bin/history-migration.py run-ready \
--batch-id "$batch_id"
else
echo "存在验证失败或非预期状态,暂不进入正式执行。"
fi
fi
如果文章真正进入 validation_failed,就不要再把它当成网络偶发超时。应先修复中文文章,再使用 validate-live --post-id 文章ID --refresh 重新验证。
十、正式生成中文摘要并覆盖英文译文
1. 设置智谱 API Key
生产只读验证只需要 WP_ADMIN_COOKIE 和 WP_REST_NONCE。正式生成中文摘要时还需要项目现有的智谱 API Key,也就是 ZHIPU_API_KEY。
同样不要把真实 API Key 直接写进命令历史。可以在当前终端中静默输入:
if [ -z "${ZHIPU_API_KEY-}" ]; then
read -r -s -p "请输入智谱 API Key:" ZHIPU_API_KEY
echo
export ZHIPU_API_KEY
fi
ZHIPU_API_KEY 也只在当前终端会话中生效,不应写入博客、截图、Git 仓库或公开配置文件。
正式执行前统一检查三项变量:
missing=0
for name in WP_ADMIN_COOKIE WP_REST_NONCE ZHIPU_API_KEY
do
if [ -z "${!name-}" ]; then
echo "缺少环境变量:$name"
missing=1
else
echo "已设置:$name"
fi
done
if [ "$missing" -ne 0 ]; then
echo "环境变量不完整,暂不执行正式批次。"
fi
这项检查仍然只验证变量是否非空。Cookie、REST Nonce 是否有效,要以实际 REST 预检结果为准;如果多篇文章同时返回 403,应先更新认证信息,而不是继续消耗单篇重试次数。
2. 正式执行批次
确认三项变量均已设置,而且当前认证信息有效后执行:
python3 bin/history-migration.py run-ready \
--batch-id "$batch_id" \
--execute
程序会逐篇执行中文摘要生成、写入校验和英文覆盖翻译。真正的超时、DNS、连接中断和短暂服务不可用仍由有限重试机制处理,因此看到一次暂时性网络错误时不必立即中止整个批次。
但 HTTP 500 不能再一律视为可重试的服务端波动。程序现在会解析 WordPress 返回的 JSON code 和 message:例如 swq_full_article_token_validation_failed 会被归类为 protected_token_validation_error,第一次失败后立即停止。认证类 403 也要区别看待;多篇文章在同一 REST 预检位置连续返回 403 时,应先更新 WP_ADMIN_COOKIE 或 WP_REST_NONCE,而不是继续消耗单篇重试次数。
十一、批次结束后如何判断真正完成
正式执行结束后,再执行:
python3 bin/history-migration.py run-ready \
--batch-id "$batch_id"
python3 bin/history-migration.py status
python3 bin/history-migration.py summary
真正完成不能只看 selected_count=0。它只表示当前没有可由 run-ready 直接执行的文章,并不代表不存在 translation_failed、validation_failed 或 blocked。
最终应同时满足:当前批次 completed=批次总数、remaining=0、没有失败状态、integrity=ok,并且全局输出显示“最新未完成批次: 无”“建议创建下一批: True”。
十二、异常状态与错误分类
从 2026 年 8 月 4 日开始,异常恢复不再要求人工先判断应该使用 resume 还是 restart-from-current。但在进入统一恢复命令以前,仍然需要先区分“验证阶段失败”和“正式执行阶段失败”,因为两者处理的证据与安全边界不同。
1. awaiting_readonly_validation + SSH timeout
如果生产只读查询报 batch read-only SSH query timed out,并且文章状态仍然是 awaiting_readonly_validation,说明验证尚未形成成功或失败结论,可以对仍处于该状态的文章有限重试。
SSH 退出码 255 现在会在底层最多尝试三次并短暂退避。三次均失败时,应报告 production_readonly_unavailable 和“内容变化未知”,期间不得写入状态,也不能把 changed=False 误当成“中文源没有变化”。
2. validation_failed
这表示生产只读验证已经形成明确结论,不属于网络偶发错误。应先修复 WordPress 中文文章,再执行:
python3 bin/history-migration.py validate-live \
--post-id 文章ID \
--refresh
只有重新验证通过并进入 ready_for_execution 后,才继续正式执行。
3. 正式执行失败要区分暂时性与确定性错误
正式执行阶段仍可能出现真正的暂时性错误,例如 GLM 请求超时、DNS 失败、连接中断或短暂服务不可用。这类错误继续使用有限重试,批次不会因为单篇偶发失败而立即中止。
但 HTTP 500 本身不能再直接等同于网络故障。程序现在会继续解析 WordPress 返回的 JSON code、message 和 data。例如 swq_full_article_token_validation_failed 会被分类为 protected_token_validation_error,属于确定性的内容或保护标记校验错误,第一次失败后就停止,不再无意义地自动重试三次。
批次输出和状态文件会保留真实错误类别、WordPress 错误信息以及 execution evidence 路径。遇到这类错误时,应先查看错误信息并修复对应中文源,再使用后文的统一 recover 命令。
4. 摘要阶段 HTTP 400 / 1301 内容安全过滤
2026 年 8 月 14 日又补充了一类不能盲目重试的摘要阶段错误:智谱接口返回 HTTP 400,但真实响应中包含 error.code=1301,并提示输入或生成内容可能包含不安全或敏感内容。这类失败不是普通网络波动,也不能只根据 HTTP 400 猜测为请求长度或参数问题。
现在 HTTP JSON 层会同时识别顶层和嵌套的 error.code / error.message,execution evidence 也会保留脱敏后的 HTTP status、错误响应以及请求尺寸统计。需要先看真实 evidence:如果已经明确是 1301 内容安全过滤,就停止无意义的重复请求;如果最新一次只是 TimeoutError,则仍应把它与原始内容过滤根因分开记录,不能互相覆盖。
对于这种确定性的模型拒绝,如果文章本身没有需要修改的内容,不应为了迎合某一个模型而改写历史文章。后文新增了 ChatGPT 人工兜底流程,用外部人工完成中文摘要和英文译文,再通过只读验收和 mark-manual-completed 收敛到 completed。
5. 多篇同时出现 403
如果多篇文章在相同 REST 预检位置连续返回 403,应优先检查 WP_ADMIN_COOKIE 和 WP_REST_NONCE。这属于公共认证依赖失效,不是多篇文章同时出现内容异常,也不应继续消耗单篇重试次数。
十三、统一使用 recover 恢复单篇失败文章
此前恢复流程分散在 resume、restart-from-current --apply 和 run-ready --execute 之间,需要人工读取状态、比较中文源并选择命令。现在仓库已经新增统一恢复入口,由程序根据现有状态、execution evidence、pre-write 基线和生产只读数据自动选择安全策略。这里的统一恢复主要针对已经形成稳定失败证据的单篇文章;如果本地执行被 Ctrl+C 中断,状态停在 translation_started / execution_in_progress,需要先完成孤立 attempt 与 execution evidence 的协调,再回到正常恢复路径。
1. 先只读预览恢复策略
cd ~/code/wordpress-ai-excerpt-backfill
python3 bin/history-migration.py recover \
--post-id 文章ID
预览模式不会写入状态,也不会重新执行文章。它会显示当前状态、真实错误、execution evidence、生产中文源是否变化、恢复策略、将执行的步骤以及下一步。
2. 确认后自动恢复并执行
python3 bin/history-migration.py recover \
--post-id 文章ID \
--execute
recover --execute 会在一次调用中完成必要的恢复步骤,只处理目标文章,不会重新执行同批已经 completed 的其他文章。正常情况下,不再需要随后手工运行整个批次的 run-ready --execute。
3. recover 会自动选择的策略
completed → none:文章已经完成,立即安全返回,不连接 SSH、不重新执行、不写入状态,显示“下一步:无”;translation_failed + 中文源未修改 → resume:复用现有安全基线,从失败位置继续执行;translation_failed + 中文标题或正文已修改 → restart_from_current:归档旧 execution / pre-write,以当前生产版本重建基线并在同一次调用中重新执行目标文章;excerpt_failed + 中文源未修改 → retry_excerpt_generation:摘要生成在写入 WordPress 以前失败,中文源、空摘要和英文基线仍匹配时,只重新生成中文摘要并继续安全执行;如果 evidence 已明确是1301等确定性内容安全过滤,则不要继续盲目重试,改走本节后面的 ChatGPT 人工兜底;production_readonly_unavailable → blocked:未取得生产数据,无法判断中文源是否变化,不写入任何状态;- 不满足现有安全检查的情况 →
blocked:明确打印原因,不通过无条件重置绕过检查。
原有 resume、restart-from-current --apply 等命令仍然保留用于兼容和专项排障。正常失败日常仍优先使用 recover;只有中断留下的 translation_started / execution_in_progress、孤立 attempt 或计数漂移等状态协调问题,才进入后面的专项恢复流程。
4. 真实案例:6965 的 Protected Token 校验失败
在批次 mixed-syntaxhighlighter-20260804-02 中,19 篇一次完成,中文文章 6965 在英文覆盖翻译阶段连续返回 HTTP 500。旧分类把它标记为 transient_network_error,导致批次内重试三次,随后单篇 resume 又失败一次。
读取 data/backups/single-candidate/chinese-6965.execution.json 后才确认真实错误是 swq_full_article_token_validation_failed:预期 23 个保护标记,实际得到 27 个,额外出现 4 个 SWQINLINE...END。这不是网络波动,而是 GLM-5.2 对行内保护标记的确定性重复输出。
人工简化中文标题、普通段落和图片 alt 中容易触发行内保护的技术字符串后,统一恢复流程应当只需要:
python3 bin/history-migration.py recover \
--post-id 6965
python3 bin/history-migration.py recover \
--post-id 6965 \
--execute
程序会检测中文源已经变化,自动选择 restart_from_current,重建基线并只执行 6965。该文章最终一次完成,批次恢复为 20 篇全部 completed。
这次案例也推动了错误分类修复:以后 protected_token_validation_error 首次失败后立即停止,并直接显示真实 code、message 和 evidence 路径,不再伪装成普通 HTTP 500 网络错误。
5. completed 状态必须是安全空操作
统一恢复命令上线后的第一次真实冒烟测试还发现:即使文章已经是 completed,旧实现仍会继续查询生产环境;一旦 SSH 恰好不可用,就把已完成文章错误显示为 blocked。
现在判断顺序已经调整为先读取本地工作流状态。遇到 completed 立即返回 strategy=none,不查询生产环境、不比较中文源、不重建基线、不重新执行,也不修改 execution evidence。
模式: execute
文章: zh=6965 en=13099
当前状态: completed
真实错误: 无
生产中文源: 无需检查
恢复策略: none
将执行: 无
写入操作: 否
原因: 文章已经完成,无需恢复
下一步: 无
6. Ctrl+C 中断后停在 translation_started / execution_in_progress
2026 年 8 月 8 日的真实恢复中又补充了一个统一 recover 之外的边界场景:在 resume --execute 已经发出远程请求后按下 Ctrl+C,本地 Python 进程会退出,但已经发到 WordPress / GLM 的远程请求不保证同步取消。因此不能把 Ctrl+C 理解成“本次 attempt 从未发生”。
如果之后看到 workflow 仍是 execution_in_progress,execution evidence 为 translation_started,而直接执行 resume 得到 selected_count: 0,不要删除状态文件,也不要手工修改 JSON。先执行 show-current --json 核对当前批次与真实 evidence,然后对中断发生的 resume 阶段做 attempt 对账。
python3 bin/history-migration.py show-current --json
python3 bin/history-migration.py reconcile-attempts \
--post-id 文章ID \
--stage resume \
--json
先看 preview。只有确认 eligible=true、planned_count 符合预期,并且目标确实是这篇文章后,才增加 --apply:
python3 bin/history-migration.py reconcile-attempts \
--post-id 文章ID \
--stage resume \
--apply \
--json
reconcile-attempts 负责把 started / terminated / orphaned attempt 与当前 recovery generation 对齐。2026 年 8 月 8 日修复后,这些统计只在当前 recovery_generation 中计算:旧 generation 的 attempt 仍保留用于全生命周期审计,但不会继续占用新 generation 的重试额度。
如果旧逻辑已经把历史 attempt 错误写入当前 retry_counts,即使已经没有 orphaned attempt,preview 仍可能显示 counter_drift=true 和 reconciliation_action=counter_drift_correction。这种情况下 apply 只修正当前 generation 的计数,不改 lifetime_retry_counts、execution evidence 或其他文章。
attempt 对账完成后,再预览 execution evidence 与协调状态的同步:
python3 bin/history-migration.py sync-execution \
--json
只有确认 items 中计划同步的文章符合预期,例如从 blocked / execution_in_progress 恢复到 ready_for_translation_resume,才执行:
python3 bin/history-migration.py sync-execution \
--apply \
--json
随后必须先做 resume Preview,而不是直接再次发送模型请求:
python3 bin/history-migration.py resume \
--post-id 文章ID
只有看到 selected_count: 1、allowed_count: 1 后,才执行:
python3 bin/history-migration.py resume \
--post-id 文章ID \
--execute
如果这一步在 WordPress REST 预检阶段返回 403 rest_cookie_invalid_nonce,先更新 Cookie / REST Nonce,再继续恢复,不要连续重试。一次失败的正式执行也可能被记录进当前 generation 的重试计数;认证已经明确失效时,继续重试只会浪费重试额度。
这一专项流程只用于“本地中断导致状态没有正常收敛”的情况。正常的 translation_failed 仍然优先交给 recover,不要因为出现过一次 Ctrl+C 就把所有失败文章都改走 reconcile-attempts。
7. GLM 1301 内容安全过滤:使用 ChatGPT 人工兜底
如果摘要生成已经通过 execution evidence 明确确认是 1301 内容安全过滤,并且重复 recover 只会再次触发同一个确定性拒绝,就不再继续消耗 GLM 请求。这里的目标也不是修改原文章去“绕过”模型,而是保留历史文章本身,由 ChatGPT 作为外部人工兜底完成这一篇。
人工兜底时,需要先补齐中文摘要,再由 ChatGPT 根据完整中文 Gutenberg 源生成英文标题、英文摘要和完整英文正文。人工保存到 WordPress 后,不直接手工修改本地 JSON,也不删除原来的 excerpt_generation_failed evidence,而是先运行只读 Preview:
cd ~/code/wordpress-ai-excerpt-backfill
python3 bin/history-migration.py mark-manual-completed \
--post-id 文章ID
mark-manual-completed 会重新读取生产环境,检查中英文 post ID、publish 状态、Polylang 双向关系以及英文标题和正文。对于 excerpt_failed,还会额外要求中文 post_excerpt 非空;如果中文摘要还没有补齐,会明确显示 Chinese excerpt is empty,并保持 允许人工完成: 否。
只有 Preview 明确显示“允许人工完成: 是”“写入操作: 否”以后,才执行确认:
python3 bin/history-migration.py mark-manual-completed \
--post-id 文章ID \
--confirmed
--confirmed 不会再次修改 WordPress,也不会把原来的自动执行 evidence 改写成成功。它只在本地 coordination state 中记录 workflow_status=completed,并增加 manual_completion.status=confirmed、manual_completion.method=manual_external。这样既能让批次正常收敛,又能完整保留最初的 GLM 1301、重试和 recovery 历史。
这是一条异常兜底路径,不替代正常的 run-ready --execute。正常文章仍由仓库自动生成摘要和覆盖英文译文;只有模型明确拒绝、继续自动重试没有意义时,才切换到 ChatGPT + 人工保存 + mark-manual-completed。
十四、真实验证结果与流程演进
第一批:mixed-syntaxhighlighter-20260729-01
共 20 篇。人工 Gutenberg 规范化、mark-converted、生产只读验证和正式执行最终全部完成。过程中出现过 GLM HTTP 400、英文覆盖翻译 HTTP 500、Polylang SSH timeout,当时均通过有限重试或单篇恢复完成。
第二批:mixed-syntaxhighlighter-20260730-01
共 20 篇。第一次生产只读验证 18 篇通过、2 篇因 batch read-only SSH query timed out 失败,但两篇仍停留在 awaiting_readonly_validation,有限重试后全部通过。正式执行最后出现 1 篇 translation_failed,当时使用单篇 resume 完成。
第三批:mixed-syntaxhighlighter-20260731-01
共 20 篇。执行前首先遇到 WP_REST_NONCE 失效导致多篇 REST 预检连续 403,重新设置 Nonce 后恢复。随后 8819 因英文标题翻译失败进入 translation_failed;人工修改中文标题和正文后,旧执行基线失效,直接 resume 被安全拦截。
当时新增并实际验证了 restart-from-current,以当前生产版本重建 recovery generation 后完成单篇执行。这一案例证明了恢复必须区分中文源是否变化,但旧流程仍要求人工选择多条命令。
后续案例:mixed-syntaxhighlighter-20260804-02
该批次 20 篇中,19 篇正常完成;6965 因 Protected Token 校验失败停在 translation_failed。这一案例进一步暴露了 HTTP 500 分类过于粗糙、确定性错误被无效重试,以及恢复命令过于分散的问题。
完成错误分类、SSH 255 有限重试和统一 recover 改造后,相关定向测试 21 项全部通过,全量测试由 402 项增加到 404 项并全部通过;真实环境中 6965 恢复完成,completed 的 preview 与 --execute 安全空操作也验证通过。最终改动以提交 52d6fac 推送到远程仓库。
2026 年 8 月 14 日案例:mixed-syntaxhighlighter-20260814-03
这一批 20 篇中,19 篇自动完成,中文文章 7756 在中文摘要生成阶段连续返回 HTTP 400。最初批次内三次均为 400,第一次 recover --execute 又遇到一次独立的 TimeoutError,随后再次恢复时仍返回 400。进一步读取最新 execution evidence 后确认,智谱真实响应为 error.code=1301,并带有内容安全过滤信息。
离线使用生产代码 extract_excerpt_source() 检查后,7756 的原始正文为 8228 字符,真正送入摘要模型的 cleaned content 只有 892 字符,完整 payload 约 3427 UTF-8 字节,没有触发 20k / 28k 截断。因此这次没有证据支持“输入过长”,最强证据就是模型内容安全过滤。
最终没有为了通过 GLM 而修改历史文章,而是由 ChatGPT 人工生成中文摘要、英文标题、英文摘要和完整英文正文,保存到 WordPress 后先执行 mark-manual-completed --post-id 7756 只读验收,再使用 --confirmed 收敛状态。最终 7756 记录为 workflow_status=completed、manual_completion.method=manual_external,原始 excerpt_generation_failed evidence 继续保留;该批次恢复为 completed=20、remaining=0。
这些案例说明,是否能开始下一批不应依据历史累计的 retry_exhausted,而应以当前 failed=0、blocked=0、remaining=0、“最新未完成批次: 无”和“建议创建下一批: True”为准。
十五、现在每天真正需要做的固定流程
经过多批真实运行和统一恢复改造以后,日常操作可以压缩为下面九步:
- 执行
status和summary,确认是否存在未完成批次; - 只有在“建议创建下一批: True”时才从剩余空摘要候选中创建新的固定批次,每批最多 20 篇;
- 设置当天的
batch_id和csv_file; - 一次性输出当前批次后台编辑地址;
- 逐篇检查当前结构:已经是干净 Gutenberg 的文章只确认即可;Classic / Mixed / unknown 规范化为 Gutenberg;如仍有 SyntaxHighlighter 或其他旧代码结构,再按现有规则迁移和核对;
- 批量执行
mark-converted; - 检查 Cookie / REST Nonce 后执行生产只读验证;变量非空不代表认证仍有效,遇到
403 rest_cookie_invalid_nonce时先更新认证信息,对仍停留在awaiting_readonly_validation的 SSH 偶发失败再有限重试; - 确认
WP_ADMIN_COOKIE、WP_REST_NONCE、ZHIPU_API_KEY后执行run-ready --execute; - 最终用
run-readyPreview、status、summary同时确认 completed、remaining、失败状态和完整性,再决定是否开始下一批。
如果正式执行后已经形成稳定的单篇失败状态,不再先手工判断 resume 或 restart-from-current。统一执行 recover --post-id 文章ID 查看策略;只有程序明确提示需要人工修改中文文章时才进入 WordPress,保存后再执行同一条命令并增加 --execute。如果 evidence 已明确确认是 1301 等确定性的模型内容安全过滤,继续自动重试没有意义,则按第十三节的 ChatGPT 人工兜底流程补齐中文摘要和英文内容,再用 mark-manual-completed Preview / --confirmed 收敛为 completed。如果是 Ctrl+C 等本地中断留下 translation_started / execution_in_progress,则属于状态协调异常,先按第十三节的专项流程执行 reconcile-attempts 和 sync-execution,再回到 resume Preview / Execute。
生产验证阶段仍然单独处理:awaiting_readonly_validation + SSH timeout → 有限重试;validation_failed → 修复后 validate-live --refresh。正式执行阶段的失败优先统一交给 recover;只有模型明确拒绝、正常自动恢复已经没有意义时,才进入 ChatGPT 人工兜底。
总结
到 2026 年 8 月 12 日,前面的 Gutenberg + Code Block Pro、Gutenberg + SyntaxHighlighter、Mixed + SyntaxHighlighter 以及最后 5 篇异常文章已经全部完成,历史迁移状态达到 45 个固定批次、857 篇文章全部 completed。随后生产只读盘点又确认还有 424 篇符合处理条件的中文空摘要历史文章,因此同一套 SOP 继续向剩余历史文章扩展。
这一阶段真正变化的只有候选入口:不再要求文章必须属于 Mixed 或包含 SyntaxHighlighter,而是选择所有“中文已发布、摘要为空、存在已发布英文对应文章、尚未进入固定批次”的历史文章。固定批次、人工确认、mark-converted、生产只读验证、摘要生成、英文覆盖翻译、recover 和 completed 状态机都继续沿用。
Mixed Gutenberg + SyntaxHighlighter 阶段并不是前一套迁移方案失效,而是候选文章本身多了一层 Classic / Mixed 历史结构。原来的 fixed batch、人工转换、生产验证、摘要生成、英文覆盖翻译和 completed 状态机仍然可以继续使用,只需要把整篇 Gutenberg normalization 纳入人工确认和生产验证。
随着真实批次继续推进,SOP 又补齐了四层关键能力:一是根据 WordPress JSON 响应区分真正的暂时性网络错误与确定性的 Protected Token 校验错误,并进一步识别智谱嵌套 error.code=1301 的内容安全过滤;二是通过统一 recover 命令自动判断中文源是否变化,并选择 resume、restart_from_current、retry_excerpt_generation、blocked 或 none;三是针对 Ctrl+C 等本地中断留下的 translation_started / execution_in_progress,通过 generation-scoped reconcile-attempts、counter_drift 修正和 sync-execution 让状态重新收敛;四是在模型因确定性内容安全过滤无法继续时,允许由 ChatGPT 完成人工摘要和英文翻译,再通过 mark-manual-completed 安全确认外部完成,同时保留原始失败 evidence。
现在日常主流程仍然是“创建固定批次 → 人工转换 → 生产验证 → 批量执行 → 汇总确认”。正常单篇失败仍优先通过统一 recover 预览和执行;只有本地中断、孤立 attempt、execution evidence 与 workflow 不一致等少数状态协调异常,才需要进入专项恢复;模型明确返回 1301 等确定性拒绝时,则可以切换到 ChatGPT 人工兜底并用 mark-manual-completed 收尾。整个过程中都不应通过删除 state、清空 evidence 或手工修改 JSON 来强行推进状态。
需要长期技术维护或远程问题排查?
我是拥有 15+ 年经验的 PHP / Go 后端工程师,长期关注已有系统维护、Bug 修复、性能优化、服务器排查、WordPress 网站维护和小功能迭代。
如果你的项目遇到以下情况,可以先从一次小问题排查开始合作:
- ✅ PHP / Laravel / Yii2 老项目无人维护
- ✅ Go / Gin 后端接口需要排查或优化
- ✅ WordPress 网站访问慢、报错或插件冲突
- ✅ Nginx / MySQL / Redis / Linux 服务器异常
- ✅ CDN / Cloudflare / DNS / HTTPS 配置问题
- ✅ 需要长期远程技术支持或兼职维护
更多介绍请查看:关于我 & 合作
微信:13980074657
邮箱:shuijingwanwq@gmail.com
Telegram:@shuijingwan
GitHub:https://github.com/shuijingwan

