我的 WordPress 博客积累了大量历史技术文章。其中一部分文章仍然使用旧的 SyntaxHighlighter 代码区块,中文摘要为空,对应的英文文章也需要重新覆盖翻译。
如果完全依赖人工处理,每篇文章都要经历:
转换代码区块
→ 核对代码语言
→ 生成中文摘要
→ 点击覆盖翻译
→ 等待翻译完成
→ 检查结果
英文覆盖翻译通常还需要等待一段时间。一天处理 20 篇时,人工会不断在 WordPress 后台、终端和翻译状态之间来回切换。
因此,我最终整理出一套相对稳定的批处理流程:
仓库筛选并固定当天的 20 篇文章
→ 人工迁移 SyntaxHighlighter,并对 Code Block Pro 核对代码语言
→ 仓库记录人工转换完成
→ 仓库执行生产只读验证
→ 批量生成中文摘要
→ 批量覆盖英文译文
→ 单篇失败有限重试
→ 分析最终失败原因
→ 修复根因并恢复失败文章
→ 确认整个批次全部完成
本文既记录这套流程的形成过程,也是一份可以在以后每天直接照着执行的操作手册。
一、两个仓库分别负责什么
这套流程涉及两个独立仓库。
1. WordPress 翻译管线仓库
/home/wangqiang/code/wordpress-ai-translation-pipeline
主要负责:
- SlyTranslate 与 GLM 翻译定制;
- Gutenberg 结构保护;
- HTML、短代码和代码块保护;
- Plaintext 可翻译区域;
- 受保护令牌验证;
- 英文覆盖翻译;
- WordPress 生产 MU 插件。
2. 历史文章摘要补全仓库
/home/wangqiang/code/wordpress-ai-excerpt-backfill
主要负责:
- 筛选待处理历史文章;
- 创建固定批次;
- 保存中英文文章关系;
- 生成中文摘要;
- 调用生产环境覆盖翻译;
- 保存写入前备份;
- 记录执行状态;
- 批量重试与恢复;
- 汇总整个批次的完成情况。
两个仓库的职责不能混在一起:
翻译结构问题
→ wordpress-ai-translation-pipeline
摘要生成和批次调度问题
→ wordpress-ai-excerpt-backfill
二、人工和仓库的职责边界
仓库负责
仓库负责:
- 筛选摘要为空的中文文章;
- 排除已经进入旧批次的文章;
- 确认文章存在已发布的英文译文;
- 固定每天需要处理的 20 篇文章;
- 保存中文文章 ID 和英文文章 ID;
- 保存后台编辑地址;
- 记录原 SyntaxHighlighter 数量;
- 以原 SyntaxHighlighter 数量作为迁移后 Code Block Pro 数量上限;
- 执行生产只读验证;
- 调用 GLM 生成中文摘要;
- 写入中文摘要;
- 调用 SlyTranslate 覆盖英文文章;
- 保存执行证据和备份;
- 对单篇失败进行有限重试;
- 汇总 completed、failed 和 remaining。
人工负责
人工只负责:
- 根据仓库清单打开文章;
- 逐块判断旧 SyntaxHighlighter 内容:代码、命令和日志通常迁移为 Code Block Pro;更适合其他 Gutenberg 区块的内容可以迁移为对应区块;
- 检查代码内容是否完整;
- 逐块核对代码语言;
- 检查 Gutenberg 是否存在损坏区块;
- 保存文章;
- 明确确认人工转换已经完成。
人工不再负责:
- 按发布时间寻找文章;
- 逐篇判断摘要是否为空;
- 手动点击每一篇覆盖翻译;
- 守着 WordPress 后台等待翻译完成。
最终职责边界可以概括为:
仓库决定处理哪些文章
→ 人工完成必须人工判断的代码块迁移
→ 仓库完成验证、摘要生成和英文翻译
三、完整状态流转
每篇文章必须按照以下顺序推进:
awaiting_manual_conversion
→ mark-converted
→ awaiting_readonly_validation
→ validate-live
→ ready_for_execution
→ run-ready --execute
→ completed
上面是正常主路径。真实批处理遇到网络中断时,还可能暂时停在 excerpt_generated、ready_for_translation_resume 或 blocked。这些状态不能统一用一个 resume 命令处理:ready_for_execution 继续交给 run-ready;明确处于翻译续跑阶段时才使用 resume;blocked 则先核对生产实际状态,再决定是否执行协调恢复。
其中最容易遗漏的是:
mark-converted
即使已经在 WordPress 后台完成代码块转换,仓库也不会自动知道这件事。
如果文章仍然处于:
awaiting_manual_conversion
就直接运行:
validate-live
仓库会拒绝验证:
ERROR: cannot validate live from awaiting_manual_conversion
因此,人工转换完成后,必须先将结果记录到本地协调状态,再执行生产只读验证。
四、每天开始前检查仓库状态
进入仓库:
cd ~/code/wordpress-ai-excerpt-backfill
执行:
python3 bin/history-migration.py status
python3 bin/history-migration.py summary
重点查看:
最新未完成批次
下一步
建议创建下一批
如果某一轮看到:
建议创建下一批: False
建议: continue latest incomplete batch; do not create a new batch
说明仍然存在未完成批次,不能创建新的 20 篇。
只有看到:
最新未完成批次: 无
建议创建下一批: True
建议: all batches complete
才可以创建下一批。
需要注意:
python3 bin/history-migration.py summary
是全局汇总命令,不支持:
--batch-id

五、创建新的 20 篇固定批次
确认所有旧批次完成后,在普通终端执行。这里必须读取 summary 的结果,因为 建议创建下一批: True 来自 summary,不是 status:
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="syntaxhighlighter-${date_tag}-${number}"
candidate_file="data/analysis/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
mapfile -t preview_files < <(
find data/analysis \
-maxdepth 1 \
-type f \
-name '*syntaxhighlighter*preview*.csv' \
-printf '%T@ %p\n' \
| sort -nr \
| cut -d' ' -f2-
)
if [ "${#preview_files[@]}" -eq 0 ]; then
echo "没有找到 SyntaxHighlighter preview CSV。"
else
preview_file="${preview_files[0]}"
echo
echo "使用候选文件:$preview_file"
echo "新批次 ID:$batch_id"
echo "新批次文件:$batch_file"
python3 bin/build-syntaxhighlighter-batch.py \
--preview "$preview_file" \
--output "$batch_file" \
--expected-count 20 \
--batch-id "$batch_id" \
--pilot-manifest \
data/analysis/gutenberg-syntaxhighlighter-migration-pilot-candidates.csv \
--old-phase1-manifest \
data/analysis/gutenberg-cbp-empty-excerpt-candidates.csv \
&& python3 bin/history-migration.py init-state --apply \
&& echo \
&& echo "新批次已经创建:" \
&& echo "batch_id=\"$batch_id\"" \
&& echo "csv_file=\"$batch_file\""
fi
fi
fi
正常完成后,会输出类似:
batch_id="syntaxhighlighter-20260725-01"
csv_file="data/analysis/syntaxhighlighter-migration-batch-20260725-01.csv"
记下这两个值。后面的命令都需要使用它们。
这里不要再提交批次 CSV。当前 data/analysis/ 属于本地分析数据并被 Git 忽略;当天固定批次 CSV 和 data/state/history-migration/ 都继续保留在本地即可。
尤其不要为了强行提交这个 CSV 使用 git add -f,也不要执行:
git add .
git add -A
git clean
git reset --hard
真正需要提交的只有后续确实发生的源码、测试或文档修改,并且应精确指定文件。
六、查看文章清单和后台编辑地址
设置当天批次变量:
cd ~/code/wordpress-ai-excerpt-backfill
batch_id="syntaxhighlighter-20260725-01"
csv_file="data/analysis/syntaxhighlighter-migration-batch-20260725-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 handle:
rows = list(csv.DictReader(handle))
remaining = []
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")
)
workflow_status = state.get(
"workflow_status",
"uninitialized",
)
if workflow_status == "completed":
continue
edit_url = row.get("edit_url") or (
"https://admin.shuijingwanwq.com/wp-admin/"
f"post.php?post={post_id}&action=edit"
)
remaining.append(
{
"post_id": post_id,
"english_id": row["english_post_id"],
"title": row["chinese_title"],
"syntax_before": row[
"before_syntaxhighlighter_count"
],
"cbp_expected": row[
"expected_code_block_pro_count_after"
],
"status": workflow_status,
"edit_url": edit_url,
}
)
print(f"批次:{batch_id}")
print(f"批次总数:{len(rows)}")
print(f"当前待处理:{len(remaining)}")
print()
for index, item in enumerate(remaining, 1):
print(
f"{index:02d}. "
f"zh={item['post_id']} "
f"en={item['english_id']} "
f"SH={item['syntax_before']} "
f"预期CBP={item['cbp_expected']} "
f"status={item['status']}"
)
print(f" 标题:{item['title']}")
print(f" 编辑:{item['edit_url']}")
print()
PY
新批次应显示:
批次总数:20
当前待处理:20

七、人工迁移 SyntaxHighlighter
按照清单中的后台地址逐篇打开中文文章。这里的目标不是机械地把每一个 SyntaxHighlighter 都变成 Code Block Pro,而是把旧区块迁移到更合适的 Gutenberg 结构。
每篇文章需要完成:
- 逐块判断旧 SyntaxHighlighter 内容:代码、命令、日志等通常迁移为 Code Block Pro;更适合其他 Gutenberg 区块的内容可以迁移为对应区块;
- 确保最终 SyntaxHighlighter 数量为 0;
- 检查迁移后的内容是否完整,没有因为转换而丢失代码、命令、日志或正文;
- 凡是迁移成 Code Block Pro 的区块,都逐块核对代码语言;
- 检查 Gutenberg 是否存在损坏区块;
- 保存文章。
原区块明确声明了语言
迁移为 Code Block Pro 时,设置为对应语言,例如:
- PHP
- JavaScript
- Bash
- JSON
- HTML
- CSS
- SQL
- Python
- Nginx
- YAML
.yml 和 .yaml 文件都选择:
YAML
原区块没有声明语言
如果该内容仍然适合 Code Block Pro,但原区块没有声明语言,则设置为:
Plaintext
Code Block Pro 有时会继承上一次使用的语言,因此保存前必须逐块核对。
每篇保存前应确认:
SyntaxHighlighter 数量已经归零
Code Block Pro 数量不超过批次记录的原 SyntaxHighlighter 数量
允许部分旧区块迁移为其他更合适的 Gutenberg 区块
迁移后的内容没有丢失
Code Block Pro 的语言设置正确
Gutenberg 没有损坏区块提示
因此,生产只读验证采用的核心数量规则是:SH = 0,并且 CBP ≤ 原 SH 数量。Code Block Pro 比原 SyntaxHighlighter 少并不自动代表迁移失败。
不需要为了保险,每篇保存后都额外切到源代码编辑器搜索 SyntaxHighlighter。只有只读验证明确返回 syntaxhighlighter-remains 时,再进入源代码编辑器搜索旧区块并修复即可。


八、记录人工转换已经完成
WordPress 后台保存成功后,还需要将人工完成结果写入本地协调状态。
本节负责:
awaiting_manual_conversion
→ awaiting_readonly_validation
执行:
cd ~/code/wordpress-ai-excerpt-backfill
batch_id="syntaxhighlighter-20260725-01"
csv_file="data/analysis/syntaxhighlighter-migration-batch-20260725-01.csv"
read -r -p \
"确认当前批次待处理文章均已完成代码块转换和逐块语言核对,输入 YES 继续:" \
answer
if [ "$answer" != "YES" ]; then
echo "未确认,已停止,没有修改状态。"
else
python3 - "$batch_id" "$csv_file" <<'PY'
import csv
import importlib.util
import json
import sys
from pathlib import Path
root = Path.cwd().resolve()
batch_id = sys.argv[1]
csv_path = Path(sys.argv[2])
state_dir = (
root
/ "data/state/history-migration"
/ batch_id
)
module_path = root / "bin/history-migration.py"
spec = importlib.util.spec_from_file_location(
"history_migration_runtime",
module_path,
)
if spec is None or spec.loader is None:
raise SystemExit(
"无法加载 history-migration.py"
)
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
with csv_path.open(
encoding="utf-8-sig",
newline="",
) as handle:
rows = list(csv.DictReader(handle))
def read_state(post_id):
state_path = (
state_dir
/ f"chinese-{post_id}.json"
)
if not state_path.is_file():
raise RuntimeError(
f"缺少状态文件:{state_path}"
)
return json.loads(
state_path.read_text(encoding="utf-8")
)
targets = []
for row in rows:
post_id = int(row["chinese_post_id"])
status = read_state(post_id).get(
"workflow_status"
)
if status == "awaiting_manual_conversion":
targets.append(row)
print(f"批次:{batch_id}")
print(f"等待记录人工转换:{len(targets)}")
print()
success = []
failed = []
for index, row in enumerate(targets, 1):
post_id = int(row["chinese_post_id"])
syntax_before = int(
row["before_syntaxhighlighter_count"]
)
cbp_after = int(
row[
"expected_code_block_pro_count_after"
]
)
try:
module.mark_converted(
root,
post_id,
syntax_before,
cbp_after,
True,
)
final_status = read_state(
post_id
).get("workflow_status")
if (
final_status
== "awaiting_readonly_validation"
):
success.append(post_id)
print(
f"[{index}/{len(targets)}] "
f"已记录:zh={post_id} "
f"status={final_status}"
)
else:
failed.append(post_id)
print(
f"[{index}/{len(targets)}] "
f"状态异常:zh={post_id} "
f"status={final_status}"
)
except Exception as error:
failed.append(post_id)
print(
f"[{index}/{len(targets)}] "
f"记录失败:zh={post_id} "
f"{type(error).__name__}: {error}"
)
print()
print("========== 人工转换记录汇总 ==========")
print(f"待记录:{len(targets)}")
print(f"成功:{len(success)}")
print(f"失败:{len(failed)}")
if failed:
print(
"失败文章:"
+ ", ".join(
str(value) for value in failed
)
)
raise SystemExit(1)
PY
fi
新批次全部记录成功后,应显示:
========== 人工转换记录汇总 ==========
待记录:20
成功:20
失败:0
这一步只更新本地状态,不会:
- 调用 GLM;
- 调用 SlyTranslate;
- 修改 WordPress;
- 生成中文摘要;
- 覆盖英文文章。
九、执行生产只读验证
本节负责:
awaiting_readonly_validation
→ ready_for_execution
validate-live 是单篇命令,只支持 --post-id,不支持 --batch-id。批次验证仍然由脚本读取当天 CSV,再逐篇调用。
先确认登录环境变量仍然存在:
for name in WP_ADMIN_COOKIE WP_REST_NONCE
do
if [ -z "${!name-}" ]; then
echo "缺少环境变量:$name"
else
echo "已设置:$name"
fi
done
如果当前终端缺少 Cookie 或 Nonce,不要把值粘贴到聊天记录或 Shell 命令参数里。可以在当前终端静默输入:
read -rsp "粘贴 Cookie,然后回车:" WP_ADMIN_COOKIE
echo
export WP_ADMIN_COOKIE
read -rsp "粘贴 X-WP-Nonce,然后回车:" WP_REST_NONCE
echo
export WP_REST_NONCE
Cookie 和 X-WP-Nonce 可以从浏览器开发者工具的 Network 请求中重新取得。确认两项均已设置后,再执行批次只读验证:
cd ~/code/wordpress-ai-excerpt-backfill
batch_id="syntaxhighlighter-20260725-01"
csv_file="data/analysis/syntaxhighlighter-migration-batch-20260725-01.csv"
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
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 handle:
rows = list(csv.DictReader(handle))
def read_state(post_id):
state_path = (
state_dir
/ f"chinese-{post_id}.json"
)
if not state_path.is_file():
raise RuntimeError(
f"缺少状态文件:{state_path}"
)
return json.loads(
state_path.read_text(encoding="utf-8")
)
targets = []
already_ready = []
completed = []
unexpected = []
for row in rows:
post_id = int(row["chinese_post_id"])
status = read_state(post_id).get(
"workflow_status"
)
if status == "awaiting_readonly_validation":
targets.append(post_id)
elif status == "ready_for_execution":
already_ready.append(post_id)
elif status == "completed":
completed.append(post_id)
else:
unexpected.append(
(post_id, status)
)
print(f"批次:{batch_id}")
print(f"本次待验证:{len(targets)}")
print(f"已经 ready:{len(already_ready)}")
print(f"已经 completed:{len(completed)}")
print()
success = []
failed = []
for index, post_id in enumerate(targets, 1):
print(
f"[{index}/{len(targets)}] "
f"开始只读验证:zh={post_id}"
)
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 = read_state(
post_id
).get("workflow_status")
if (
result.returncode == 0
and final_status
== "ready_for_execution"
):
success.append(post_id)
print(
f"[{index}/{len(targets)}] "
f"验证通过:zh={post_id} "
f"status={final_status}"
)
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"status={final_status} "
f"returncode={result.returncode} "
f"error={error}"
)
print()
status_counts = {}
for row in rows:
post_id = int(row["chinese_post_id"])
status = read_state(post_id).get(
"workflow_status",
"unknown",
)
status_counts[status] = (
status_counts.get(status, 0) + 1
)
print("========== 只读验证最终汇总 ==========")
print(f"本次待验证:{len(targets)}")
print(f"本次通过:{len(success)}")
print(f"本次失败:{len(failed)}")
print(
"批次当前 ready:"
f"{status_counts.get('ready_for_execution', 0)}"
)
print(
"批次已经 completed:"
f"{status_counts.get('completed', 0)}"
)
if unexpected:
print(
"非预期状态:"
+ ", ".join(
f"{post_id}:{status}"
for post_id, status in unexpected
)
)
if failed:
print(
"验证失败文章:"
+ ", ".join(
str(value) for value in 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
新批次全部通过时,应看到 20 篇进入 ready_for_execution,并且 run-ready 预览为 selected_count=20、allowed_count=20。
只读验证的关键迁移规则是:
SyntaxHighlighter = 0
Code Block Pro <= 原 SyntaxHighlighter 数量
Gutenberg 结构完整
未知代码格式为 0
中英文文章关系、状态和现有哈希检查继续通过
如果某篇只是 SSH、Timeout、RemoteDisconnected 等网络故障,且没有形成新的验证证据,直接对这一篇重新执行普通 validate-live 即可:
python3 bin/history-migration.py validate-live --post-id 文章ID
如果某篇已经进入 validation_failed,并且已经在 WordPress 后台人工修复内容,则必须强制重新读取生产环境,使用:
python3 bin/history-migration.py validate-live --post-id 文章ID --refresh
例如出现 syntaxhighlighter-remains 时,再进入源代码编辑器搜索 <!-- wp:syntaxhighlighter/code,修复残留旧区块后执行 --refresh。

十、批量生成摘要并覆盖英文译文
正式执行需要三个环境变量:
WP_ADMIN_COOKIE
WP_REST_NONCE
ZHIPU_API_KEY
如果 ZHIPU_API_KEY 在新的终端会话中不存在,可以静默输入并导出:
read -rsp "粘贴 ZHIPU_API_KEY,然后回车:" ZHIPU_API_KEY
echo
export ZHIPU_API_KEY
确认三个变量均已设置后执行:
cd ~/code/wordpress-ai-excerpt-backfill
batch_id="syntaxhighlighter-20260725-01"
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 "已停止,未执行批次。"
else
python3 bin/history-migration.py run-ready \
--batch-id "$batch_id" \
--execute
fi
执行过程中会实时显示“第 1/20 篇”“第 2/20 篇”等进度;单篇失败后会有限次数自动重试,最终失败也不会立即终止整个批次,而是继续处理后面的文章。
现在的 excerpt_generated 恢复逻辑已经补齐。某次运行已经生成摘要、但后续网络中断时,下一次尝试会先只读观察生产中文摘要:
- 生产中文摘要非空:自动切换到翻译续跑,不重新调用 GLM;
- 生产中文摘要仍为空:重新走普通 run,允许再次调用 GLM 生成并覆盖摘要;
- 只读观察遇到 Timeout、RemoteDisconnected、HTTP 502/503:不猜测、不错误推进状态,在当前有限次数内继续重试;
- 已经 completed 的文章不会重新执行。
这里的优先级是可靠完成高于极致节省 Token。只要生产摘要确认仍为空,重新调用一次 GLM 是可以接受的;但摘要已经保存时会直接续跑翻译。
批次命令结束后,不要因为还有未完成文章就立刻把 resume 当成统一恢复命令。先查看 status、summary 和 show-current,再根据实际状态选择 run-ready、resume 或协调恢复。

十一、有限重试能解决什么
当前批量入口会对单篇文章进行有限次数自动尝试,并在单篇最终失败后继续处理下一篇。这对于无人值守批次非常重要。
它适合解决:
- 临时请求超时;
- RemoteDisconnected 等短暂网络失败;
- 偶发 HTTP 502 或 503;
- 服务端短时间不可用;
- 一次性的模型响应异常;
- 摘要已生成后发生的网络中断,只要生产实际状态能够安全确认。
但它不能替代确定性问题的根因修复,例如提示词与校验规则冲突、占位符遗漏、Gutenberg 结构损坏、校验正则误判等。
三次尝试后仍有未完成文章时,先看协调状态,而不是统一执行 resume:
ready_for_execution
→ 继续使用 run-ready
ready_for_translation_resume / translation_failed
→ 使用 resume
blocked
→ 先核对 last_failure 和生产实际状态,再做协调恢复
excerpt_failed
→ 先检查被拒绝摘要或确定性校验原因
resume 不是“所有未完成文章的统一恢复命令”。例如文章仍是 ready_for_execution 时,直接执行 resume 可能得到 selected_count: 0,这并不代表批次已经完成。
十二、几轮真实批次中暴露的三个问题
此前一个 20 篇批次首次批量执行后:
16 篇完成
3 篇 translation_failed
1 篇 excerpt_failed
表面上看,最终失败率达到了 20%。
进一步检查后发现,4 篇失败并不是随机网络问题,而是两个确定性规则问题。
1. Plaintext 行数校验过严
三篇文章分别报错:
Plaintext line count changed during translation:
source=3, translated=1
Plaintext line count changed during translation:
source=11, translated=10
Plaintext line count changed during translation:
source=94, translated=93
中文翻译成英文时,模型自然可能合并或拆分换行。
因此,对于允许翻译的普通 Plaintext 内容,不应该要求译前译后物理行数完全相同。
最终修复为:
- 取消可翻译 Plaintext 的逐行等长要求;
- 保留 START/STOP 区域边界;
- 保留令牌数量、唯一性、编号和顺序校验;
- 非空 Plaintext 不能整体变空;
- Plaintext 不能吞入相邻 Gutenberg 或受保护区域;
- 代码、命令、路径、URL、状态码等不可翻译内容继续原样保护;
- 结构错误使用 HTTP 422 和
retryable=false。
生产 MU 插件版本更新为:
2026.07.24.18
提示词策略版本更新为:
2026-07-24-v12
离线测试结果:
115 项通过
0 项失败
部署后,三篇失败文章依次执行:
python3 bin/history-migration.py resume \
--post-id 文章ID \
--execute
三篇全部成功完成。
2. Ctrl + S 被误判为 Markdown 列表
剩余一篇文章生成的摘要实际上是正常单段文本,例如:
在 VS Code 中按下 Ctrl + S 时文件内容意外被还原……
但本地校验器使用的旧正则是:
r"(^|\s)(?:#{1,6}\s|[-*+]\s|\d+[.)]\s)|[*_]{2}"
其中:
(^|\s)
允许匹配正文任意位置的空白。
因此:
Ctrl + S
中的:
+
被误判成 Markdown 的 + 项目符号。
修复后的正则只允许在行首识别列表:
r"(?m)^[ \t]*(?:#{1,6}[ \t]+|[-*+][ \t]+|\d+[.)][ \t]+)"
修复后:
Ctrl + S不再误判;A + B不再误判;x - y不再误判;C++不再误判;- 行首的
- 第一项、+ 第一项、1. 第一项仍然会被拒绝。
同时,GLM 初始摘要提示词也明确要求:
- 一个连续的中文纯文本段落;
- 不使用 Markdown;
- 不使用项目符号和编号列表;
- 不使用标题、引用、表格或代码块;
- 不换行;
- 不添加“摘要:”等前缀;
- 内容可以直接保存到 WordPress
post_excerpt。
完整测试结果:
334 项通过
0 项失败
3. excerpt_generated 后恢复到普通 run,导致摘要非空时反复预检失败
随后一次严格按照本文 SOP 进行的 20 篇真实演练,又暴露了一个恢复分支问题。首次执行后有文章停在 execution=excerpt_generated:GLM 已经生成摘要,甚至摘要已经成功写入 WordPress,但后续 GET、翻译请求或网络连接发生中断。
旧逻辑会把 prepared 和 excerpt_generated 都重新映射成普通 run/restart。如果生产摘要实际上已经保存,下一次普通 preflight 会因为:
chinese_excerpt_empty: false
preflight_passed: false
而稳定失败,继续重试也只是在重复同一个错误。
最终修复保持得很小:重试 excerpt_generated 前,先通过现有只读生产数据源确认中文摘要实际状态:
- 摘要已保存:进入
ready_for_translation_resume,自动使用 resume 继续英文覆盖翻译; - 摘要仍为空:回到
ready_for_execution,允许重新调用 GLM; - 生产状态暂时无法确认:记录为可重试的观察失败,不盲目执行 run 或 resume。
修复后,剩余 4 篇真实文章全部在下一轮第一次尝试中完成;完整测试套件为 344 项通过。对应修复提交为 b02bbf4。
十三、批次结束后的完整检查
批量执行结束后,不能只看:
selected_count: 0
因为它只代表当前没有处于 ready_for_execution 的文章,不一定代表全部完成。
必须同时执行:
cd ~/code/wordpress-ai-excerpt-backfill
batch_id="syntaxhighlighter-20260725-01"
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
allowed_count: 0
completed=20
remaining=0
excerpt_failed=0
translation_failed=0
next_action=当前批次已完成
十四、列出未完成文章和失败原因
如果看到:
completed=16
remaining=4
先执行只读诊断:
cd ~/code/wordpress-ai-excerpt-backfill
batch_id="syntaxhighlighter-20260725-01"
python3 - "$batch_id" <<'PY'
import json
import sys
from pathlib import Path
batch_id = sys.argv[1]
state_dir = (
Path("data/state/history-migration")
/ batch_id
)
execution_dir = Path(
"data/backups/single-candidate"
)
items = []
for state_path in state_dir.glob(
"chinese-*.json"
):
state = json.loads(
state_path.read_text(encoding="utf-8")
)
if state.get("workflow_status") == "completed":
continue
post_id = int(
state["chinese_post_id"]
)
execution_path = (
execution_dir
/ f"chinese-{post_id}.execution.json"
)
execution = {}
if execution_path.is_file():
execution = json.loads(
execution_path.read_text(
encoding="utf-8"
)
)
failure = state.get("last_failure") or {}
items.append(
{
"position": state.get(
"batch_position",
999999,
),
"post_id": post_id,
"english_id": state.get(
"english_post_id"
),
"workflow": state.get(
"workflow_status"
),
"execution": execution.get(
"status"
),
"error": execution.get("error"),
"error_response": execution.get(
"error_response"
),
"last_stage": failure.get("stage"),
"last_reason": failure.get("reason"),
"retry_counts": state.get(
"retry_counts"
),
}
)
items.sort(
key=lambda item: item["position"]
)
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']}"
)
print(
f" workflow={item['workflow']} "
f"execution={item['execution']}"
)
print(
f" last_stage={item['last_stage']} "
f"last_reason={item['last_reason']}"
)
print(f" error={item['error']}")
print(
f" error_response="
f"{item['error_response']}"
)
print(
f" retry_counts="
f"{item['retry_counts']}"
)
print()
PY
十五、不同未完成状态怎样处理
1. ready_for_execution
这类文章仍然由 run-ready 处理,不要改用通用 resume。可以先预览:
python3 bin/history-migration.py run-ready \
--batch-id "$batch_id"
如果文章的 execution evidence 已经是 excerpt_generated,当前代码会在真正重试前自动观察生产中文摘要:已保存则续跑翻译,仍为空则重新 run;不需要人工先把状态改成 translation resume。
2. translation_failed 或 ready_for_translation_resume
如果中文摘要已经确认写入,而英文覆盖翻译需要继续,使用:
python3 bin/history-migration.py resume \
--post-id 文章ID \
--execute
这个命令只继续英文翻译,不重新生成中文摘要。但执行前仍要看真实错误;确定性的结构或占位符问题要先修复翻译管线。
3. excerpt_failed
先检查最近保存的被拒绝摘要:
cd ~/code/wordpress-ai-excerpt-backfill
post_id="文章ID"
python3 - "$post_id" <<'PY'
import sys
from pathlib import Path
post_id = sys.argv[1]
rejected_dir = Path(
"data/backups/single-candidate/rejected"
)
files = sorted(
rejected_dir.glob(
f"chinese-{post_id}-glm47-rejected-attempt-*.txt"
),
key=lambda path: path.stat().st_mtime,
)[-3:]
print(f"找到最近被拒绝的摘要:{len(files)}")
print()
for index, path in enumerate(files, 1):
text = path.read_text(encoding="utf-8")
print("=" * 80)
print(f"第 {index} 次:{path}")
print("-" * 80)
print(text)
print("-" * 80)
print(repr(text))
print()
PY
检查摘要是真的包含 Markdown、列表或其他不合格格式,还是被校验器误判。不要在不知道真实模型输出的情况下无限重试。
如果需要在修复提示词或校验器后直接重新执行单篇完整流程,可以执行:
cd ~/code/wordpress-ai-excerpt-backfill
batch_id="syntaxhighlighter-20260725-01"
post_id="文章ID"
python3 bin/execute-single-candidate.py \
--post-id "$post_id" \
--manifest \
"data/analysis/history-migration-validation/${batch_id}/chinese-${post_id}.execution-candidate.csv" \
--expected-candidate-count 1 \
--backup-dir data/backups/single-candidate \
--execute
成功时会显示:
{"chinese_post_id": 文章ID, "english_post_id": 英文文章ID, "status": "completed"}
直接调用 execute-single-candidate.py 后,如果执行证据已经完成但协调状态没有同步,再先预览:
python3 bin/history-migration.py sync-execution \
--json
确认只计划同步目标文章后,再执行:
python3 bin/history-migration.py sync-execution \
--apply \
--json
4. blocked / stale_execution_state
如果 status 提示需要人工排查 blocked,先用 show-current 找到目标文章,再读取该文章状态里的 last_failure、execution_evidence 和 workflow_status。不要直接清状态文件。
如果确认是 run 阶段留下的孤立尝试,并且 execution evidence 为 excerpt_generated,需要先确认生产中文摘要究竟为空还是已保存。之后先做 preview:
# 生产中文摘要仍为空
python3 bin/history-migration.py reconcile-attempts \
--post-id 文章ID \
--stage run \
--chinese-excerpt-empty
# 生产中文摘要已经保存
python3 bin/history-migration.py reconcile-attempts \
--post-id 文章ID \
--stage run \
--chinese-excerpt-saved
只有 preview 显示 eligible=True、allowed=True 后,才在同一条命令末尾增加 --apply。摘要为空会恢复到 ready_for_execution;摘要已保存会恢复到 ready_for_translation_resume。
如果 blocked 的原因不是上述孤立 run 尝试,就按真实 last_failure 排查,不能套用这组命令。
十六、怎样判断整个批次真正完成
最终执行:
cd ~/code/wordpress-ai-excerpt-backfill
python3 bin/history-migration.py status
python3 bin/history-migration.py summary
真正完成时,重点不是某一个命令的 selected_count,而是整个协调状态已经收敛:
completed = 批次总数
ready = 0
in_progress = 0
translation_resume = 0
excerpt_failed = 0
translation_failed = 0
validation_failed = 0
blocked = 0
remaining = 0
integrity = ok
2026 年 7 月 26 日完成本轮 20 篇真实回归后,仓库状态已经达到:
固定批次: 6
固定文章: 108
执行证据: completed=108 failed=0 pending=0 translation_started=0 other=0 no_execution_evidence=0
完整性: ok conflicts=0 errors=0
流程: ready=0 in_progress=0 translation_resume=0 excerpt_failed=0 translation_failed=0 validation_failed=0 blocked=0 remaining=0
下一步: 当前批次已完成
syntaxhighlighter-20260725-01: total=20 completed=20 remaining=0
最新未完成批次: 无
建议创建下一批: True
建议: all batches complete
retry_exhausted 是历史累计重试耗尽记录,不代表当前仍有同样数量的失败文章。本轮最终状态中即使它仍是非零,只要当前 remaining=0、所有失败/blocked 桶为 0,就已经完成。
十七、本地运行状态不要提交
批次处理后,git status 可能出现:
M data/state/history-migration/某批次/chinese-文章ID.json
M data/state/history-migration/某批次/events.jsonl
?? data/state/history-migration/某批次/
这些属于本地运行状态和执行证据。
应当:
- 保留;
- 不提交;
- 不删除;
- 不执行
git add .; - 不执行
git add -A; - 不为了让工作区看起来干净而清除状态。
需要保留的内容包括:
data/state/
events.jsonl
data/backups/
*.execution.json
*.pre-write.json
rejected/
代码修改应精确指定文件暂存,例如:
git add \
src/candidate_execution.py \
src/glm47_excerpt_client.py \
tests/test_candidate_execution.py \
tests/test_excerpt_clients.py
十八、每天真正需要执行的固定顺序
以后每天按照下面这条固定路径执行。批次 CSV 和运行状态都保留本地,不再加入“提交批次 CSV”这一步:
1. 执行 status 和 summary
2. 确认最新未完成批次为无,并且建议创建下一批: True
3. 创建新的 20 篇固定批次并执行 init-state
4. 记下 batch_id 和 csv_file;批次 CSV 保留本地,不提交 Git
5. 输出 20 篇文章清单和后台编辑地址
6. 人工迁移 SyntaxHighlighter;最终要求 SH=0
7. 迁移为 Code Block Pro 的区块逐块核对语言;CBP 数量允许小于原 SH 数量
8. 批量执行 mark-converted
9. 检查 WP_ADMIN_COOKIE 和 WP_REST_NONCE;缺失时在当前终端静默重新加载
10. 批量执行 validate-live
11. 网络型验证失败直接重试;人工修复后的 validation_failed 使用 --refresh
12. run-ready 预览,确认目标文章全部 allowed
13. 检查 ZHIPU_API_KEY;缺失时静默重新加载
14. 执行 run-ready --execute,让单篇有限重试并继续后续文章
15. 执行 status、summary 和必要时 show-current
16. ready_for_execution 继续交给 run-ready
17. ready_for_translation_resume / translation_failed 才使用 resume
18. blocked 先核对真实状态,必要时使用 reconcile-attempts preview 后再 --apply
19. excerpt_failed 先检查真实模型输出或确定性校验原因
20. 确认 completed=20、remaining=0、blocked=0、integrity=ok
21. 保留 data/state、events、backup 和 execution evidence,不清理本地运行状态
最关键的原则是:
不要把 resume 当成所有未完成文章的统一恢复命令。
不要因为 selected_count=0,就认定批次已经完成。
不要因为 Code Block Pro 数量少于原 SyntaxHighlighter,就自动判定迁移失败。
不要为了节省少量 GLM Token,把恢复逻辑做得比实际需求更复杂。
不要在没有查看真实错误、真实 execution evidence 和生产实际状态前盲目改状态。
十九、最终结论
这套流程的目标并不是建立一个理论上最复杂的自动化系统,而是在可接受的时间、成本和维护量下,稳定完成 WordPress 历史文章迁移。
最终工作方式是:
仓库筛选并固定文章
→ 人工按内容选择合适的 Gutenberg 迁移方式
→ 生产只读验证确认 SH=0、CBP 不超过原数量并检查其他安全条件
→ 仓库批量生成摘要和覆盖英文译文
→ 单篇偶发故障有限重试并继续后续文章
→ excerpt_generated 自动根据生产摘要选择 resume 或 restart
→ 人工只介入真正需要判断的 blocked 或确定性错误
→ completed=批次总数、remaining=0 后才进入下一批
这次严格按照文章 SOP 重新走一遍非常有价值,因为真正暴露出来的问题并不是“批处理框架需要整体重写”,而是几个很具体的流程缺口:创建批次时读取错命令输出、忽略目录里的 CSV 不应提交、迁移数量规则过严、验证失败缺少恢复分支、环境变量缺少重新加载说明,以及 excerpt_generated 的恢复路径不完整。
最后一个问题修复后,真实剩余 4 篇全部在下一轮第一次尝试中完成;整个仓库最终达到 108/108 completed,最新 20 篇批次达到 20/20 completed。
因此,接下来不需要继续扩展一个更复杂的多轮调度系统。更合适的做法是保持现有实现简单:单篇有限重试、失败隔离、状态可恢复;每天只按本文固定顺序执行,并以最终 remaining=0 作为完成标准。
需要长期技术维护或远程问题排查?
我是拥有 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


评论
一条对“每天批量处理 20 篇 WordPress 历史文章:代码块迁移、摘要补全与英文覆盖翻译完整 SOP”的回复
[…] […]