没有不值得去解决的问题,也没有不值得去学习的技术!

Batch-Processing 20 Mixed WordPress Legacy Posts per Day: A Complete SOP for Classic-to-Gutenberg Conversion, SyntaxHighlighter Migration, Excerpt Backfill, and English Overwrite Translation

图 2:复杂 Classic 一次转换成多个 Gutenberg 区块

作者:

In my previous post, I documented:

“Batch-Processing 20 WordPress Legacy Posts a Day: Complete SOP for Code Block Migration, Excerpt Backfill, and English Overwrite Translation”

That stage mainly covered legacy posts that were already in Gutenberg format but still contained SyntaxHighlighter in the post body.

After completing that stage, I found that quite a few Chinese legacy posts on the site still contained SyntaxHighlighter.

Further inspection confirmed that these posts had not been missed by the earlier scan. They belonged to a different legacy format:

Plaintext
Gutenberg 区块
+
Classic / 经典编辑器内容
+
SyntaxHighlighter

The analyzer classified them as:

Plaintext
editor_format = mixed

In the earlier Gutenberg + SyntaxHighlighter stage, only posts with editor_format=gutenberg were processed, so these Mixed posts were left for the next stage.

In the historical snapshot, there were 717 Mixed candidates in total, and 712 of them had one main issue:

Plaintext
editor-format-mixed

The remaining five also had other code-format or structural issues, so I left them aside for separate handling.

I therefore started the second stage:

Plaintext
Mixed Gutenberg + SyntaxHighlighter
→ Classic / Mixed 内容规范化为 Gutenberg
→ SyntaxHighlighter 转 Code Block Pro
→ 核对代码语言
→ 生产只读验证
→ 自动生成中文摘要
→ 自动覆盖英文译文
→ completed

The overall workflow continues to follow the operating pattern established in the previous post, with only the steps genuinely required for the Mixed stage added.


1. What the Two Repositories Are Responsible For

The workflow still involves two repositories.

1. WordPress Translation Pipeline

Plaintext
/home/wangqiang/code/wordpress-ai-translation-pipeline

Main responsibilities:

  • SlyTranslate and GLM translation customization;
  • Gutenberg structure protection;
  • HTML, code, and shortcode protection;
  • placeholder validation;
  • full English overwrite translation;
  • MU plugins related to the WordPress production environment.

2. Legacy Post Migration Repository

Plaintext
/home/wangqiang/code/wordpress-ai-excerpt-backfill

Main responsibilities:

  • manage legacy candidates;
  • create fixed batches;
  • store Chinese-English post relationships;
  • manage manual conversion status;
  • run production read-only validation;
  • generate Chinese excerpts;
  • call the existing English overwrite-translation endpoint;
  • store execution evidence;
  • perform limited retries for sporadic failures;
  • summarize the status of the entire batch.

The overall division of responsibilities remains:

Plaintext
人工处理文章格式
→ wordpress-ai-excerpt-backfill 验证和调度
→ wordpress-ai-translation-pipeline 完成英文覆盖翻译

2. Division of Responsibilities Between Manual Work and the Repository

Repository Responsibilities

In the Mixed stage, the repository is responsible for:

  • selecting eligible Mixed + SyntaxHighlighter posts from historical data;
  • excluding posts that have already entered an earlier batch or have already been completed;
  • sorting them from newest to oldest by publication time;
  • fixing at most 20 posts per batch;
  • storing Chinese and English post IDs;
  • storing the original SyntaxHighlighter count;
  • manage manual conversion status;
  • verifying that the final post has been normalized into valid Gutenberg content;
  • verifying that the SyntaxHighlighter count has dropped to zero;
  • validating Code Block Pro;
  • automatically generating the Chinese excerpt;
  • automatically overwriting the existing English translation;
  • performing limited retries for a single-post failure;
  • summarizing the final status of the entire batch.

Manual Responsibilities

Manual work is responsible for:

  • opening the 20 posts specified by the repository;
  • converting Classic / Mixed content to Gutenberg;
  • checking the body layout after automatic conversion;
  • splitting paragraphs, headings, numbering, and similar structure again when necessary;
  • converting SyntaxHighlighter to Code Block Pro;
  • checking the language setting of each Code Block Pro block;
  • checking images, captions, links, and body structure;
  • saving the Chinese post;
  • finally confirming that manual processing for the batch is complete.

Manual work does not need to:

  • find the next batch of posts manually;
  • sort posts by publication time manually;
  • generate Chinese excerpts manually;
  • click through the English overwrite-translation action one post at a time.

3. Complete State Flow

The normal main flow remains the same as in the previous post:

Plaintext
awaiting_manual_conversion
→ mark-converted
→ awaiting_readonly_validation
→ validate-live
→ ready_for_execution
→ run-ready --execute
→ completed

The differences are mainly in the manual-processing and production-validation stages.

In the Mixed stage, in addition to code-block conversion and language review, I also need to confirm:

Plaintext
整篇 Gutenberg normalization 已完成

Production read-only validation also adds these requirements:

Plaintext
editor_format = gutenberg
classic_outside_blocks = false

Therefore:

Plaintext
Classic 区块已经消失

is not directly equivalent to:

Plaintext
Gutenberg normalization 已经完成

I still need to manually confirm the body structure and layout after conversion.


4. Check Repository Status Before Starting Each Day

Enter the repository:

Bash
cd ~/code/wordpress-ai-excerpt-backfill

Run:

Bash
python3 bin/history-migration.py status
python3 bin/history-migration.py summary

Focus on:

Plaintext
最新未完成批次
下一步
建议创建下一批

If the output shows:

Plaintext
最新未完成批次: mixed-syntaxhighlighter-XXXXXXXX-XX
建议创建下一批: False

continue and finish the current batch.

Do not create another 20 posts in advance.

Only when the output shows:

Plaintext
最新未完成批次: 无
建议创建下一批: True
建议: all batches complete

should the next batch be created.


5. Create a New Fixed Batch of 20 Mixed Posts

The Mixed stage uses:

Plaintext
bin/build-mixed-syntaxhighlighter-batch.py

Candidate ordering is fixed as:

Plaintext
published_at DESC
chinese_post_id DESC

In other words, newer legacy posts are processed first.

The selection does not prioritize:

Plaintext
SyntaxHighlighter 最少
内容最短
HTML 最简单
人工最好处理

If fewer than 20 candidates remain at the end, all remaining candidates are processed directly.

After confirming that the previous batch is complete:

Bash
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
        preview_file="data/analysis/gutenberg-syntaxhighlighter-empty-excerpt-preview.csv"
        translations_file="data/raw/wordpress-zh-translation-links-20260721.jsonl"

        mapfile -t raw_files < <(
            find data/raw \
                -maxdepth 1 \
                -type f \
                \( \
                    -name 'wordpress-zh-posts-20260720*.jsonl' \
                    -o \
                    -name 'wordpress-zh-posts-20260721*.jsonl' \
                \) \
                | sort
        )

        if [ "${#raw_files[@]}" -eq 0 ]; then
            echo "没有找到 WordPress 原始历史快照。"
        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"

            python3 bin/build-mixed-syntaxhighlighter-batch.py \
                "${raw_files[@]}" \
                --preview "$preview_file" \
                --translations "$translations_file" \
                --output "$batch_file" \
                --batch-id "$batch_id" \
                --maximum 20 \
            && python3 bin/history-migration.py init-state --apply \
            && echo \
            && echo "新批次已经创建:" \
            && echo "batch_id=\"$batch_id\"" \
            && echo "csv_file=\"$batch_file\""
        fi
    fi
fi

The final output should normally look similar to:

Plaintext
batch_id="mixed-syntaxhighlighter-20260730-01"
csv_file="data/analysis/mixed-syntaxhighlighter-migration-batch-20260730-01.csv"

6. Set Batch Variables and View Admin Edit URLs

For easier daily operation, I set the two variables that change each day separately.

For example:

Bash
batch_id="mixed-syntaxhighlighter-20260730-01"
csv_file="data/analysis/mixed-syntaxhighlighter-migration-batch-20260730-01.csv"

The commands below reuse:

Plaintext
$batch_id
$csv_file

so there is no need to search through long commands for dates and batch IDs.

Then output the admin edit URLs for the current batch in one go:

Bash
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

This provides all admin links needed for that day’s work at once.


7. Manually Normalize Gutenberg and Migrate SyntaxHighlighter

The main change in the Mixed stage is in this section.

Each post requires two types of manual work:

Plaintext
Classic / Mixed → Gutenberg
SyntaxHighlighter → Code Block Pro

1. Convert Classic to Gutenberg

After selecting a Classic block, use the WordPress option:

Plaintext
转换为区块

In a simple case, for example, when the Classic block contains only:

Plaintext
3. 最终成功运行日志

the conversion can directly produce:

HTML
<!-- wp:paragraph -->
<p>3. 最终成功运行日志</p>
<!-- /wp:paragraph -->

Simple content like this usually needs no further adjustment.

Figure 1: Simple Classic content converted into a Gutenberg Paragraph
Figure 1: Simple Classic content converted into a Gutenberg Paragraph

For more complex Classic content, a single Classic block can also be split into multiple Gutenberg blocks in one conversion.

For example, the converted result may contain:

Plaintext
Paragraph
Paragraph
Image
Paragraph
Code Block Pro
Paragraph
……

There is no need to recreate every paragraph inside the Classic block manually.

Figure 2: A complex Classic block converted into multiple Gutenberg blocks in one operation
Figure 2: A complex Classic block converted into multiple Gutenberg blocks in one operation

2. Always Check the Layout After Automatic Conversion

This is an important point in actual use.

A successful WordPress conversion to Gutenberg does not guarantee that the body structure is correct.

After a complex Classic block is converted, content that was originally separate, such as:

Plaintext
章节标题
普通段落
1. 第一项
2. 第二项
结论
下一节标题

may be merged into one large Paragraph block.

Other problems may also appear:

Plaintext
原来的换行消失
编号和正文连在一起
小标题变成普通段落
多个逻辑段落合并

Therefore:

Plaintext
已经点击“转换为区块”

does not mean that the manual work is complete.

Figure 3: Classic conversion succeeded, but part of the body structure and layout became incorrect
Figure 3: Classic conversion succeeded, but part of the body structure and layout became incorrect

I then continue with manual cleanup of:

  • paragraphs;
  • headings and subheadings;
  • numbering;
  • lists;
  • line breaks;
  • image positions;
  • caption;
  • links;
  • body-content order.

until the normal reading structure is restored.

Figure 4: Gutenberg body content after manual reformatting
Figure 4: Gutenberg body content after manual reformatting

The final acceptance criteria should therefore be:

Plaintext
Classic 已转换
+
正文 Gutenberg 结构正常
+
排版正常
+
内容没有遗漏

3. Convert SyntaxHighlighter to Code Block Pro

After the Gutenberg body has been cleaned up, process SyntaxHighlighter.

Normal code, configuration, terminal output, and similar content is migrated to:

Plaintext
Code Block Pro

The final post must satisfy:

Plaintext
SyntaxHighlighter = 0

4. Review the Code Block Pro Language

When the original SyntaxHighlighter block specifies a language, use the corresponding language, for example:

Plaintext
PHP
JavaScript
Bash
JSON
HTML
CSS
SQL
Python
Nginx
YAML

When the original block does not declare a language:

Plaintext
Plaintext

Code Block Pro may inherit the language used by the previous code block, so conversion success alone is not enough; the language must be checked block by block.

5. Checklist Before Saving

Before saving each post, confirm at least:

Plaintext
Classic / Mixed 内容已经规范化为 Gutenberg
自动转换后的排版已经人工检查
SyntaxHighlighter = 0
Code Block Pro 内容完整
Code Block Pro language 正确
图片、caption、链接没有遗漏
正文内容没有重复
内容顺序没有发生异常
不存在 Gutenberg 损坏区块

Do not generate the excerpt manually here, and do not modify the English translation.


8. Record That Manual Conversion Is Complete

After all 20 posts have been processed manually, move them from:

Plaintext
awaiting_manual_conversion

to:

Plaintext
awaiting_readonly_validation

First confirm the batch variables:

Bash
echo "$batch_id"
echo "$csv_file"

Then run:

Bash
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

Normally, the key output to watch is:

Plaintext
========== 人工转换记录汇总 ==========
待记录:20
成功:20
失败:0

The command no longer prints all historical batches or the complete show-current, preventing the important result from being buried in excessive output.


9. Run Production Read-Only Validation

This stage is responsible for:

Plaintext
awaiting_readonly_validation
→ ready_for_execution

1. Check Environment Variables

Bash
for name in WP_ADMIN_COOKIE WP_REST_NONCE
do
    if [ -z "${!name-}" ]; then
        echo "缺少环境变量:$name"
    else
        echo "已设置:$name"
    fi
done

If either variable is missing:

Bash
read -rsp "粘贴 WP_ADMIN_COOKIE,然后回车:" WP_ADMIN_COOKIE
echo
export WP_ADMIN_COOKIE

read -rsp "粘贴 WP_REST_NONCE,然后回车:" WP_REST_NONCE
echo
export WP_REST_NONCE

2. Run Read-Only Validation in Batch

In the second real batch, 18 of 20 posts passed on the first run, while two returned:

Plaintext
batch read-only SSH query timed out

Their states remained:

Plaintext
awaiting_readonly_validation

After validation was retried, both posts passed on the first retry.

So limited automatic retries are built directly into this step.

Bash
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)}] "
                    f"验证通过: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

A normal result should look like:

Plaintext
========== 只读验证最终汇总 ==========
本次待验证:20
本次通过:20
本次失败:0

Then:

Plaintext
模式: preview
完整性: ok
selected_count: 20
allowed_count: 20

In addition to the original code-block checks, the Mixed stage also verifies:

Plaintext
editor_format = gutenberg
classic_outside_blocks = false

SSH Timeouts and validation_failed Are Different Cases

For example, the second batch encountered:

Plaintext
batch read-only SSH query timed out

while the state remained:

Plaintext
awaiting_readonly_validation

This is a sporadic timeout in the production read-only query and can be retried a limited number of times.

If the post actually enters:

Plaintext
validation_failed

do not continue retrying blindly. Inspect and fix the post first.

After fixing it, run:

Bash
python3 bin/history-migration.py validate-live \
    --post-id 文章ID \
    --refresh

10. Generate Excerpts in Batch and Overwrite English Translations

Formal execution requires:

Plaintext
WP_ADMIN_COOKIE
WP_REST_NONCE
ZHIPU_API_KEY

Check:

Bash
for name in WP_ADMIN_COOKIE WP_REST_NONCE ZHIPU_API_KEY
do
    if [ -z "${!name-}" ]; then
        echo "缺少环境变量:$name"
    else
        echo "已设置:$name"
    fi
done

If only the API Key is missing:

Bash
read -rsp "粘贴 ZHIPU_API_KEY,然后回车:" ZHIPU_API_KEY
echo
export ZHIPU_API_KEY

After everything is ready:

Bash
python3 bin/history-migration.py run-ready \
    --batch-id "$batch_id" \
    --execute

Progress is displayed in real time during execution:

Plaintext
[1/20] 开始处理:zh=...
[1/20] 处理完成:zh=...
[2/20] 开始处理:zh=...
……

When a single post encounters a sporadic error, the system performs a limited number of retries instead of stopping the entire batch immediately.

The first batch actually encountered:

Plaintext
GLM HTTP 400
英文覆盖翻译 HTTP 500
Polylang SSH timeout

All of them recovered during subsequent limited retries.

There is no need to stop the entire batch manually as soon as one HTTP error appears.

Let the whole batch finish first, then decide what to do based on the final state.


11. Complete Check After the Batch Finishes

After formal execution finishes:

Bash
python3 bin/history-migration.py run-ready \
    --batch-id "$batch_id"

python3 bin/history-migration.py status
python3 bin/history-migration.py summary

A completed batch should show:

Plaintext
selected_count: 0
allowed_count: 0

At the same time, the batch state must satisfy:

Plaintext
completed = 批次总数
remaining = 0
translation_failed = 0
validation_failed = 0
blocked = 0
integrity = ok
next_action = 当前批次已完成

The global state may then further show:

Plaintext
最新未完成批次: 无
建议创建下一批: True
建议: all batches complete

selected_count=0 Does Not Mean the Batch Is Complete

After the second batch finished executing, I actually saw:

Plaintext
模式: preview
完整性: ok
selected_count: 0
allowed_count: 0

But status also showed:

Plaintext
completed=19
translation_failed=1
remaining=1
下一步: 可以 resume

In other words:

Plaintext
selected_count=0

This only means that there are currently no posts that can be executed directly by run-ready.

It does not prove that:

Plaintext
整个批次已经完成

The final check must include:

Plaintext
completed
remaining
失败状态
next_action

12. Exception States and Recovery

In daily use, do not treat every exception as simply “run it again.”

Different states should follow different recovery paths.

1. awaiting_readonly_validation + SSH timeout

For example:

Plaintext
batch read-only SSH query timed out

and the state is still:

Plaintext
awaiting_readonly_validation

This means only that the read-only query did not complete successfully.

Section 9 already includes limited retry logic, so additional manual handling is usually unnecessary.

2. validation_failed

This indicates a definite validation failure.

Inspect and fix the Chinese WordPress post first.

Then:

Bash
python3 bin/history-migration.py validate-live \
    --post-id 文章ID \
    --refresh

3. translation_failed

At the end of the second batch, one post remained:

Plaintext
zh=9147
status=translation_failed

At that point, the entire batch showed:

Plaintext
completed=19
translation_failed=1
remaining=1
next_action=可以 resume

First inspect the current batch:

Plaintext
python3 bin/history-migration.py show-current

After locating the failed post:

Bash
python3 bin/history-migration.py resume \
    --post-id 9147 \
    --execute

The actual result was:

Plaintext
模式: execute
完整性: ok
selected_count: 1
- zh=9147 result=completed category=completed returncode=0 error=

After checking again, the final result was:

Plaintext
最新未完成批次: 无
建议创建下一批: True
建议: all batches complete

Therefore:

Plaintext
translation_failed

There is no need to rerun the whole batch.

Only run resume

4. ready_for_execution

If a post is still in:

Plaintext
ready_for_execution

Preview first:

Bash
python3 bin/history-migration.py run-ready \
    --batch-id "$batch_id"

After confirming:

Bash
python3 bin/history-migration.py run-ready \
    --batch-id "$batch_id" \
    --execute

5. blocked

If the state is:

Plaintext
blocked

First inspect:

Plaintext
last_failure
execution evidence
生产实际状态

Do not rerun it directly.


13. The Fixed Daily Workflow

After validating two real batches, the daily operation can now be reduced to the following fixed workflow.

Step 1: Check Status

Bash
cd ~/code/wordpress-ai-excerpt-backfill

python3 bin/history-migration.py status
python3 bin/history-migration.py summary

Confirm:

Plaintext
最新未完成批次: 无
建议创建下一批: True

Step 2: Create a New Fixed Mixed Batch

Maximum:

Plaintext
20 篇

Ordering:

Plaintext
published_at DESC
chinese_post_id DESC

Step 3: Set the Two Variables for the Day

Bash
batch_id="mixed-syntaxhighlighter-YYYYMMDD-01"
csv_file="data/analysis/mixed-syntaxhighlighter-migration-batch-YYYYMMDD-01.csv"

Step 4: Output Admin URLs

Get the admin edit URLs for the day’s 20 Chinese posts in one operation.

Step 5: Process Each Post Manually

Plaintext
Classic / Mixed → Gutenberg

检查并修复自动转换后的排版

SyntaxHighlighter → Code Block Pro

核对 Code Block Pro language

保存

Step 6: Record Manual Completion in Batch

Target:

Plaintext
待记录:20
成功:20
失败:0

Step 7: Production Read-Only Validation

The program performs limited retries for posts that remain in:

Plaintext
awaiting_readonly_validation

.

Final target:

Plaintext
本次通过:20
本次失败:0

And:

Plaintext
selected_count: 20
allowed_count: 20

Step 8: Formal Execution

Check:

Plaintext
WP_ADMIN_COOKIE
WP_REST_NONCE
ZHIPU_API_KEY

Then:

Bash
python3 bin/history-migration.py run-ready \
    --batch-id "$batch_id" \
    --execute

Step 9: Final Check

Bash
python3 bin/history-migration.py run-ready \
    --batch-id "$batch_id"

python3 bin/history-migration.py status
python3 bin/history-migration.py summary

The final state must confirm:

Plaintext
completed = 批次总数
remaining = 0
无失败状态
integrity = ok

Only after these conditions are met should the next batch be started.


14. Results from Two Consecutive Real Batches

To confirm that the workflow works in practice rather than only in theory, I processed two real batches in succession.

First Batch

Batch:

Plaintext
mixed-syntaxhighlighter-20260729-01

Total:

Plaintext
20 篇

Result:

Plaintext
人工 Gutenberg 规范化:20/20

mark-converted:20/20

生产只读验证:20/20

run-ready:
selected_count=20
allowed_count=20

最终:
completed=20

During formal execution, the batch encountered:

Plaintext
GLM HTTP 400
英文覆盖翻译 HTTP 500
Polylang SSH timeout

but all of them recovered through limited retries.

Second Batch

Batch:

Plaintext
mixed-syntaxhighlighter-20260730-01

Again:

Plaintext
20 篇

The first production read-only validation run returned:

Plaintext
通过:18
失败:2

Both failed posts had the same cause:

Plaintext
batch read-only SSH query timed out

and both remained in:

Plaintext
awaiting_readonly_validation

After limited retries:

Plaintext
待重试:2
成功:2
失败:0

Then:

Plaintext
selected_count: 20
allowed_count: 20

After formal execution finished:

Plaintext
completed=19
translation_failed=1
remaining=1

For the only failed post, I ran:

Plaintext
resume

After it succeeded, the final state was:

Plaintext
最新未完成批次: 无
建议创建下一批: True
建议: all batches complete

Across the two batches, the total was therefore:

Plaintext
40 篇

All posts were ultimately completed.


Summary

After the Gutenberg + SyntaxHighlighter stage was completed, the large number of remaining SyntaxHighlighter posts did not mean that the earlier workflow had missed them.

The real reason was that these posts belonged to:

Plaintext
Mixed Gutenberg / Classic + SyntaxHighlighter

So the candidate rules designed only for Gutenberg posts could not be applied directly.

However, once the actual migration began, I did not redesign the entire system.

The existing flow:

Plaintext
fixed batch
→ manual conversion
→ mark-converted
→ readonly validation
→ excerpt
→ English overwrite translation
→ completed

could still be reused.

The only core addition in the Mixed stage was:

Plaintext
Classic / Mixed → Gutenberg

along with the manual confirmation:

Plaintext
--gutenberg-normalization-confirmed

Production validation also adds:

Plaintext
editor_format = gutenberg
classic_outside_blocks = false

After running two consecutive batches covering 40 real legacy posts, I also verified the following cases beyond the normal main flow:

Plaintext
只读 SSH 超时后的有限重试
HTTP 400 / 500 等偶发错误自动重试
translation_failed 单篇 resume
selected_count=0 时继续检查 remaining 和失败状态

The workflow has therefore evolved from a one-time migration test into a repeatable daily SOP. Going forward, I only need to continue processing the remaining Mixed legacy posts with this fixed workflow until the migration is complete.

系列导航

需要长期技术维护或远程问题排查?

我是拥有 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

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理