我的 WordPress 博客积累了大量历史技术文章。其中一部分文章仍然使用旧的 SyntaxHighlighter 代码区块,中文摘要为空,对应的英文文章也需要重新覆盖翻译。
如果完全依赖人工处理,每篇文章都要经历:
转换代码区块
→ 核对代码语言
→ 生成中文摘要
→ 点击覆盖翻译
→ 等待翻译完成
→ 检查结果
英文覆盖翻译通常还需要等待一段时间。一天处理 20 篇时,人工会不断在 WordPress 后台、终端和翻译状态之间来回切换。
因此,我最终整理出一套相对稳定的批处理流程:
仓库筛选并固定当天的 20 篇文章
→ 人工迁移 SyntaxHighlighter 并核对代码语言
→ 仓库记录人工转换完成
→ 仓库执行生产只读验证
→ 批量生成中文摘要
→ 批量覆盖英文译文
→ 单篇失败有限重试
→ 分析最终失败原因
→ 修复根因并恢复失败文章
→ 确认整个批次全部完成
本文既记录这套流程的形成过程,也是一份可以在以后每天直接照着执行的操作手册。
一、两个仓库分别负责什么
这套流程涉及两个独立仓库。
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 数量;
- 计算迁移后预期的 Code Block Pro 数量;
- 执行生产只读验证;
- 调用 GLM 生成中文摘要;
- 写入中文摘要;
- 调用 SlyTranslate 覆盖英文文章;
- 保存执行证据和备份;
- 对单篇失败进行有限重试;
- 汇总 completed、failed 和 remaining。
人工负责
人工只负责:
- 根据仓库清单打开文章;
- 将 SyntaxHighlighter 转换为 Code Block Pro;
- 检查代码内容是否完整;
- 逐块核对代码语言;
- 检查 Gutenberg 是否存在损坏区块;
- 保存文章;
- 明确确认人工转换已经完成。
人工不再负责:
- 按发布时间寻找文章;
- 逐篇判断摘要是否为空;
- 手动点击每一篇覆盖翻译;
- 守着 WordPress 后台等待翻译完成。
最终职责边界可以概括为:
仓库决定处理哪些文章
→ 人工完成必须人工判断的代码块迁移
→ 仓库完成验证、摘要生成和英文翻译
三、完整状态流转
每篇文章必须按照以下顺序推进:
awaiting_manual_conversion
→ mark-converted
→ awaiting_readonly_validation
→ validate-live
→ ready_for_execution
→ run-ready --execute
→ completed
其中最容易遗漏的是:
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 篇固定批次
确认所有旧批次完成后,在普通终端执行:
cd ~/code/wordpress-ai-excerpt-backfill
status_output="$(
python3 bin/history-migration.py status
)"
printf '%s\n' "$status_output"
if ! grep -q '建议创建下一批: True' <<<"$status_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 "使用候选文件:$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 属于固定清单,可以单独提交:
git add "$batch_file"
git diff --cached --check
git commit -m "chore: 固定 ${batch_id} 批次"
git push origin main
不要执行:
git add .
git add -A
以免把 data/state 等本地运行状态一起提交。
六、查看文章清单和后台编辑地址
设置当天批次变量:
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 是否存在损坏区块;
- 保存文章。
原区块明确声明了语言
设置为对应语言,例如:
- PHP
- JavaScript
- Bash
- JSON
- HTML
- CSS
- SQL
- Python
- Nginx
- YAML
.yml 和 .yaml 文件都选择:
YAML
原区块没有声明语言
设置为:
Plaintext
Code Block Pro 有时会继承上一次使用的语言,因此保存前必须逐块核对。
每篇保存前应确认:
SyntaxHighlighter 数量已经归零
Code Block Pro 数量符合批次清单预期
代码内容没有丢失
代码语言设置正确
Gutenberg 没有损坏区块提示


八、记录人工转换已经完成
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 中读取文章 ID,再逐篇执行验证。
先确认登录环境变量仍然存在:
for name in WP_ADMIN_COOKIE WP_REST_NONCE
do
if [ -z "${!name-}" ]; then
echo "缺少环境变量:$name"
else
echo "已设置:$name"
fi
done
然后执行:
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
本次通过:20
本次失败:0
批次当前 ready:20
批次已经 completed:0
以及:
模式: preview
完整性: ok
selected_count: 20
allowed_count: 20

十、批量生成摘要并覆盖英文译文
正式执行需要三个环境变量:
WP_ADMIN_COOKIE
WP_REST_NONCE
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
执行过程中会实时显示每篇文章的结果。

十一、有限重试能解决什么
当前批量入口会对单篇文章进行有限次数自动尝试。
它适合解决:
- 临时请求超时;
- 短暂网络失败;
- 偶发 HTTP 502 或 503;
- 服务端短时间不可用;
- 一次性的模型响应异常。
但它不能自动解决:
- 提示词和校验规则冲突;
- Plaintext 结构规则过严;
- 占位符遗漏;
- Gutenberg 结构损坏;
- 校验正则误判;
- 模型每次都返回同一种不合格格式。
因此,三次重试后仍然失败时,不能继续盲目增加重试次数。
正确流程应该是:
自动有限重试
→ 输出最终失败原因
→ 判断是瞬时故障还是确定性故障
→ 瞬时故障才继续 resume
→ 确定性故障先修复根因
十二、这次批次中暴露的两个真实问题
本次 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 项失败
十三、批次结束后的完整检查
批量执行结束后,不能只看:
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. translation_failed
如果中文摘要已经写入,而英文覆盖翻译失败,使用:
python3 bin/history-migration.py resume \
--post-id 文章ID \
--execute
这个命令只继续英文翻译,不重新生成中文摘要。
但是,在执行前必须先看真实错误。
如果是临时网络错误,可以直接 resume。
如果是:
Plaintext line count changed
占位符遗漏
Gutenberg 结构损坏
等确定性错误,应先修复翻译管线,再执行 resume。
2. 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 或列表,还是被校验器误判。
不要在不知道真实内容的情况下继续无限重试。
3. 修复后重新执行单篇完整流程
excerpt_failed 不能使用 resume。
修复提示词或校验器后,执行:
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"}
4. 同步单篇执行证据
直接调用 execute-single-candidate.py 后,执行证据可能已经是 completed,但协调状态仍然保留旧的 excerpt_failed。
先预览:
python3 bin/history-migration.py sync-execution \
--json
确认只计划同步目标文章后,执行:
python3 bin/history-migration.py sync-execution \
--apply \
--json
这一步只更新本地协调状态,不会重新调用 GLM,也不会再次修改 WordPress。
十六、怎样判断整个批次真正完成
最终执行:
cd ~/code/wordpress-ai-excerpt-backfill
python3 bin/history-migration.py status
python3 bin/history-migration.py summary
本次成功完成后的真实结果为:
固定文章: 88
执行证据: completed=88
failed=0
pending=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
最新未完成批次: 无
建议创建下一批: True
建议: all batches complete
当前 20 篇批次显示:
total=20
completed=20
remaining=0
next_action=当前批次已完成
需要注意:
retry_exhausted=9
是历史累计重试耗尽记录,不代表当前仍有 9 篇失败。
判断当前是否完成,应看:
excerpt_failed
translation_failed
blocked
remaining
而不是只看累计的 retry_exhausted。
十七、本地运行状态不要提交
批次处理后,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
十八、每天真正需要执行的固定顺序
以后每天按照以下顺序处理:
1. 执行 status 和 summary
2. 确认没有未完成批次
3. 创建新的 20 篇固定批次
4. 提交并推送新批次 CSV
5. 输出文章清单和后台编辑地址
6. 人工迁移 SyntaxHighlighter
7. 逐块核对 Code Block Pro 语言
8. 批量记录 mark-converted
9. 批量执行 validate-live
10. 确认 selected_count=20、allowed_count=20
11. 执行 run-ready --execute
12. 查看 status 和 summary
13. 如果 remaining>0,先读取准确失败原因
14. translation_failed 根据原因执行 resume
15. excerpt_failed 先检查被拒绝摘要
16. 确定性错误先修复根因
17. 必要时执行单篇完整流程
18. 执行 sync-execution 同步协调状态
19. 确认 completed=20、remaining=0
20. 保留本地运行状态
最关键的原则是:
不要因为有自动重试,就把所有失败都视为网络波动。
不要因为 selected_count=0,就认定批次已经完成。
不要在没有查看真实错误和真实模型输出前继续盲目重试。
十九、最终结论
这套流程的目标并不是建立一个理论上最复杂的自动化系统,而是在可接受的时间、成本和维护量下,稳定完成 WordPress 历史文章迁移。
最终工作方式是:
仓库筛选文章
→ 人工处理代码块
→ 仓库完成验证、摘要和英文覆盖翻译
→ 自动重试偶发故障
→ 人工只处理真正的确定性异常
本次 20 篇文章最终全部完成,也验证了几个重要结论:
- 有限重试可以解决偶发问题,但不能替代准确的错误分类;
- 中文翻译成英文时,Plaintext 物理行数发生变化是合理现象;
- 初始提示词应该从第一次请求开始与本地校验完全对齐;
- 校验规则必须使用真实技术文本测试,避免把
Ctrl + S之类的表达误判为 Markdown; - 直接单篇执行成功后,还要同步协调状态;
- 只有
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


发表回复