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

Batch Process 20 Legacy WordPress Posts Daily: A Complete SOP for Code Block Migration, Excerpt Completion, and English Override Translation

Figure 1: The repository shows that all historical batches are complete and allows creating the next batch.

作者:

My WordPress blog has accumulated a large number of historical technical articles. Some of these articles still use the old SyntaxHighlighter code blocks, have empty Chinese excerpts, and their corresponding English articles need to be overwritten with new translations.

If handled entirely manually, each article requires:

Plaintext
Convert code blocks
→ Verify code language
→ Generate Chinese excerpt
→ Click overwrite translation
→ Wait for translation to complete
→ Check results

The English overwrite translation usually requires waiting for a while. When processing 20 articles a day, I constantly switch back and forth between the WordPress backend, the terminal, and translation statuses.

Therefore, I eventually put together a relatively stable batch processing workflow:

Plaintext
Repository selects and fixes 20 articles for the day
→ Manually migrate SyntaxHighlighter and verify code language for Code Block Pro
→ Repository records manual conversion completion
→ Repository executes production read-only validation
→ Batch generate Chinese excerpts
→ Batch overwrite English translations
→ Limited retries for single-article failures
→ Analyze root cause of final failures
→ Fix root cause and recover failed articles
→ Confirm the entire batch is complete

This article documents how this workflow was developed and serves as an operation manual that can be directly executed daily going forward.


1. What Each Repository Is Responsible For

This workflow involves two independent repositories.

1. WordPress Translation Pipeline Repository

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

Primarily responsible for:

  • SlyTranslate and GLM translation customization;
  • Gutenberg structure protection;
  • HTML, shortcode, and code block protection;
  • Plaintext translatable areas;
  • Protected token validation;
  • English overwrite translation;
  • WordPress production MU plugin.

2. Historical Article Excerpt Backfill Repository

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

Primarily responsible for:

  • Selecting historical articles to be processed;
  • Creating fixed batches;
  • Saving Chinese-English article relationships;
  • Generating Chinese excerpts;
  • Invoking production environment overwrite translation;
  • Saving pre-write backups;
  • Recording execution status;
  • Batch retry and recovery;
  • Summarizing the completion status of the entire batch.

The responsibilities of the two repositories must not be mixed:

Plaintext
Translation structure issues
→ wordpress-ai-translation-pipeline

Excerpt generation and batch scheduling issues
→ wordpress-ai-excerpt-backfill

2. Boundary of Responsibilities Between Manual and Repository

Repository Responsibilities

The repository is responsible for:

  • Selecting Chinese articles with empty excerpts;
  • Excluding articles already assigned to previous batches;
  • Confirming the existence of a published English translation for the article;
  • Fixing the 20 articles to be processed each day;
  • Saving Chinese article IDs and English article IDs;
  • Saving backend edit URLs;
  • Recording the original SyntaxHighlighter count;
  • Using the original SyntaxHighlighter count as the upper limit for the Code Block Pro count after migration;
  • Executing production read-only validation;
  • Invoking GLM to generate Chinese excerpts;
  • Writing Chinese excerpts;
  • Invoking SlyTranslate to overwrite English articles;
  • Saving execution evidence and backups;
  • Performing limited retries for single-article failures;
  • Summarizing completed, failed, and remaining.

Manual Responsibilities

Manual operations are only responsible for:

  • Opening articles based on the repository list;
  • Evaluating old SyntaxHighlighter content block by block: code, commands, and logs are typically migrated to Code Block Pro; content better suited for other Gutenberg blocks can be migrated to the corresponding blocks;
  • Checking if the code content is complete;
  • Verifying the code language block by block;
  • Checking Gutenberg for broken blocks;
  • Saving the article;
  • Explicitly confirming that manual conversion is complete.

Manual operations are no longer responsible for:

  • Finding articles by publication date;
  • Checking article by article whether the excerpt is empty;
  • Manually clicking overwrite translation for each article;
  • Waiting in the WordPress backend for translations to complete.

The final boundary of responsibilities can be summarized as:

Plaintext
Repository decides which articles to process
→ Manual completes code block migration requiring human judgment
→ Repository completes validation, excerpt generation, and English translation

3. Complete State Transitions

Each article must progress in the following order:

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

The above is the normal main path. When real batch processing encounters network interruptions, it may also temporarily stop at excerpt_generated, ready_for_translation_resume, or blocked. These states cannot be handled uniformly by a single resume command: ready_for_execution continues to be handled by run-ready; resume is only used when explicitly in the translation resume phase; for blocked, first verify the actual production state before deciding whether to execute a coordinated recovery.

The most easily overlooked step is:

Plaintext
mark-converted

Even if you have completed the code block conversion in the WordPress backend, the repository will not automatically know about it.

If the article is still in:

Plaintext
awaiting_manual_conversion

and you directly run:

Plaintext
validate-live

The repository will refuse to validate:

Plaintext
ERROR: cannot validate live from awaiting_manual_conversion

Therefore, after manual conversion is complete, you must first record the result to the local coordination state before executing production read-only validation.


4. Check Repository Status Before Starting Each Day

Enter the repository:

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

Execute:

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

Focus on checking:

Plaintext
Latest incomplete batch
Next step
Suggest creating next batch

If you see in a certain round:

Plaintext
Suggest creating next batch: False
Suggestion: continue latest incomplete batch; do not create a new batch

It means there is still an incomplete batch, and you cannot create a new batch of 20 articles.

Only when you see:

Plaintext
Latest incomplete batch: None
Suggest creating next batch: True
Suggestion: all batches complete

can you create the next batch.

Note that:

Bash
python3 bin/history-migration.py summary

is a global summary command and does not support:

Plaintext
--batch-id
Figure 1: The repository shows that all historical batches are complete and allows creating the next batch.
Figure 1: The repository shows that all historical batches are complete and allows creating the next batch.

5. Create a New Fixed Batch of 20 Articles

After confirming that all old batches are complete, execute this in a normal terminal. Here you must read the result of summary because 建议创建下一批: True comes from summary, not status:

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="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

Upon successful completion, it will output something like:

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

Note down these two values. Subsequent commands will need to use them.

Do not commit the batch CSV here. The current data/analysis/ is local analysis data and is ignored by Git; the daily fixed batch CSV and data/state/history-migration/ can just remain local.

In particular, do not use git add -f to force commit this CSV, and do not execute:

Bash
git add .
git add -A
git clean
git reset --hard

The only things that truly need to be committed are subsequent source code, test, or documentation changes that actually occur, and the exact files should be specified.


6. View Article List and Backend Edit URLs

Set the current day’s batch variable:

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

batch_id="syntaxhighlighter-20260725-01"
csv_file="data/analysis/syntaxhighlighter-migration-batch-20260725-01.csv"

Execute:

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 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

The new batch should show:

Plaintext
Total in batch: 20
Currently pending: 20
Figure 2: The repository lists the 20 articles for the day, their code block counts, and backend edit URLs.
Figure 2: The repository lists the 20 articles for the day, their code block counts, and backend edit URLs.

7. Manually Migrate SyntaxHighlighter

Open the Chinese articles one by one using the backend URLs from the list. The goal here is not to mechanically convert every SyntaxHighlighter into Code Block Pro, but to migrate the old blocks to a more appropriate Gutenberg structure.

Each article needs to complete:

  • Evaluating old SyntaxHighlighter content block by block: code, commands, logs, etc., are typically migrated to Code Block Pro; content better suited for other Gutenberg blocks can be migrated to the corresponding blocks;
  • Ensuring the final SyntaxHighlighter count is 0;
  • Checking if the migrated content is complete, with no code, commands, logs, or body text lost due to the conversion;
  • Verifying the code language block by block for any blocks migrated to Code Block Pro;
  • Checking Gutenberg for broken blocks;
  • Saving the article.

Original block explicitly declares a language

When migrating to Code Block Pro, set it to the corresponding language, for example:

  • PHP
  • JavaScript
  • Bash
  • JSON
  • HTML
  • CSS
  • SQL
  • Python
  • Nginx
  • YAML

Both .yml and .yaml files should select:

Plaintext
YAML

Original block does not declare a language

If the content is still suitable for Code Block Pro, but the original block did not declare a language, set it to:

Plaintext
Plaintext

Code Block Pro sometimes inherits the language used last time, so you must verify block by block before saving.

Before saving each article, confirm:

Plaintext
SyntaxHighlighter count is zero
Code Block Pro count does not exceed the original SyntaxHighlighter count recorded for the batch
Some old blocks are allowed to be migrated to other more appropriate Gutenberg blocks
Migrated content is not lost
Code Block Pro language settings are correct
Gutenberg has no broken block warnings

Therefore, the core count rule adopted by production read-only validation is: SH = 0, and CBP ≤ 原 SH 数量. Having fewer Code Block Pro blocks than the original SyntaxHighlighter does not automatically mean migration failure.

There is no need to play it safe by switching to the source code editor after saving each article to search for SyntaxHighlighter. Only if read-only validation explicitly returns syntaxhighlighter-remains should you enter the source code editor, search for old blocks, and fix them.

Figure 3: The original SyntaxHighlighter code block in a historical article.
Figure 3: The original SyntaxHighlighter code block in a historical article.
Figure 4: After converting to Code Block Pro, verify the code language one by one.
Figure 4: After converting to Code Block Pro, verify the code language one by one.

8. Record That Manual Conversion Is Complete

After saving successfully in the WordPress backend, you also need to write the manual completion result to the local coordination state.

This section handles:

Plaintext
awaiting_manual_conversion
→ awaiting_readonly_validation

Execute:

Bash
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

After all records for the new batch are successfully saved, it should show:

Plaintext
========== Manual Conversion Record Summary ==========
To record: 20
Success: 20
Failed: 0

This step only updates the local state and will not:

  • Invoke GLM;
  • Invoke SlyTranslate;
  • Modify WordPress;
  • Generate Chinese excerpts;
  • Overwrite English articles.

9. Execute Production Read-Only Validation

This section handles:

Plaintext
awaiting_readonly_validation
→ ready_for_execution

validate-live is a single-article command that only supports --post-id, not --batch-id. Batch validation is still handled by the script reading the current day’s CSV and invoking it article by article.

First, confirm that the login environment variables still exist:

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

If the current terminal lacks the Cookie or Nonce, do not paste the values into chat logs or shell command parameters. You can silently input them in the current terminal:

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

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

The Cookie and X-WP-Nonce can be retrieved again from the Network requests in the browser’s developer tools. After confirming both are set, execute the batch read-only validation:

Bash
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

When all articles in the new batch pass, you should see 20 articles enter ready_for_execution, and run-ready preview as selected_count=20, allowed_count=20.

The key migration rules for read-only validation are:

Plaintext
SyntaxHighlighter = 0
Code Block Pro <= original SyntaxHighlighter count
Gutenberg structure intact
Unknown code formats = 0
Chinese-English article relationship, status, and existing hash checks continue to pass

If an article only experiences a network failure such as SSH, Timeout, or RemoteDisconnected, and no new validation evidence was generated, just re-execute the normal validate-live for that article:

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

If an article has already entered validation_failed, and the content has been manually fixed in the WordPress backend, you must force a re-read of the production environment using:

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

For example, when syntaxhighlighter-remains occurs, enter the source code editor to search for <!-- wp:syntaxhighlighter/code, fix any residual old blocks, and then execute --refresh.

Figure 5: 20 articles complete production read-only validation, all entering the ready state.
Figure 5: 20 articles complete production read-only validation, all entering the ready state.

10. Batch Generate Excerpts and Overwrite English Translations

Formal execution requires three environment variables:

Plaintext
WP_ADMIN_COOKIE
WP_REST_NONCE
ZHIPU_API_KEY

If ZHIPU_API_KEY does not exist in a new terminal session, you can silently input and export it:

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

After confirming all three variables are set, execute:

Bash
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

During execution, progress such as “Article 1/20” and “Article 2/20” will be displayed in real time. A single-article failure will trigger a limited number of automatic retries, and a final failure will not immediately terminate the entire batch; it will continue processing subsequent articles.

The current excerpt_generated recovery logic has been fully implemented. If an excerpt has already been generated during a run but a subsequent network interruption occurs, the next attempt will first perform a read-only observation of the production Chinese excerpt:

  • Production Chinese excerpt is not empty: automatically switch to translation resume, without re-invoking GLM;
  • Production Chinese excerpt is still empty: re-run the normal run, allowing GLM to be called again to generate and overwrite the excerpt;
  • Read-only observation encounters Timeout, RemoteDisconnected, HTTP 502/503: no guessing, no incorrect state advancement; continue retrying within the current limited number of attempts;
  • Articles already completed will not be re-executed.

The priority here is reliable completion over极致 saving tokens. As long as the production excerpt is confirmed to still be empty, re-invoking GLM once is acceptable; but if the excerpt has already been saved, it will directly resume the translation.

After the batch command finishes, do not immediately treat resume as a unified recovery command just because there are incomplete articles. First check status, summary, and show-current, and then choose run-ready, resume, or coordinated recovery based on the actual state.

Figure 6: The terminal displays the progress of Chinese excerpt generation and English overwrite translation in real time.
Figure 6: The terminal displays the progress of Chinese excerpt generation and English overwrite translation in real time.

11. What Limited Retries Can Solve

The current batch entry point will make a limited number of automatic attempts for a single article and will continue processing the next article after a single-article final failure. This is crucial for unattended batches.

It is suitable for resolving:

  • Temporary request timeouts;
  • Brief network failures like RemoteDisconnected;
  • Sporadic HTTP 502 or 503;
  • Short-term server unavailability;
  • One-off model response anomalies;
  • Network interruptions occurring after the excerpt has been generated, as long as the actual production state can be safely confirmed.

However, it cannot replace root cause fixes for deterministic issues, such as prompt and validation rule conflicts, missing placeholders, broken Gutenberg structures, or regex validation false positives.

When there are still incomplete articles after three attempts, first check the coordination state instead of uniformly executing resume:

Plaintext
ready_for_execution
→ Continue using run-ready

ready_for_translation_resume / translation_failed
→ Use resume

blocked
→ First verify last_failure and actual production state, then perform coordinated recovery

excerpt_failed
→ First check the rejected excerpt or deterministic validation cause

resume is not a “unified recovery command for all incomplete articles.” For example, if an article is still ready_for_execution, directly executing resume might yield selected_count: 0, which does not mean the batch is complete.


12. Three Issues Exposed During Real Batches

After the first batch execution of 20 articles previously:

Plaintext
16 completed
3 translation_failed
1 excerpt_failed

On the surface, the final failure rate reached 20%.

Upon further inspection, the 4 failures were not random network issues, but rather two deterministic rule issues.

1. Plaintext Line Count Validation Too Strict

Three articles respectively reported errors:

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

When translating from Chinese to English, the model may naturally merge or split line breaks.

Therefore, for translatable normal Plaintext content, the physical line count before and after translation should not be required to be exactly the same.

The final fix was:

  • Removing the line-by-line equal length requirement for translatable Plaintext;
  • Retaining START/STOP region boundaries;
  • Retaining token count, uniqueness, numbering, and order validation;
  • Non-empty Plaintext cannot become entirely empty;
  • Plaintext cannot absorb adjacent Gutenberg or protected regions;
  • Untranslatable content such as code, commands, paths, URLs, and status codes continue to be protected as is;
  • Structural errors use HTTP 422 and retryable=false.

The production MU plugin version was updated to:

Plaintext
2026.07.24.18

The prompt strategy version was updated to:

Plaintext
2026-07-24-v12

Offline test results:

Plaintext
115 passed
0 failed

After deployment, the three failed articles were executed sequentially:

Bash
python3 bin/history-migration.py resume \
    --post-id 文章ID \
    --execute

All three completed successfully.

2. Ctrl + S Misidentified as a Markdown List

The excerpt generated for the remaining article was actually normal single-paragraph text, for example:

Plaintext
When pressing Ctrl + S in VS Code, the file content is unexpectedly reverted...

But the old regex used by the local validator was:

Python
r"(^|\s)(?:#{1,6}\s|[-*+]\s|\d+[.)]\s)|[*_]{2}"

Where:

Plaintext
(^|\s)

allows matching whitespace anywhere in the body text.

Therefore:

Plaintext
Ctrl + S

within it:

 + 

was misidentified as a Markdown + bullet point.

The fixed regex only allows list recognition at the beginning of a line:

Python
r"(?m)^[ \t]*(?:#{1,6}[ \t]+|[-*+][ \t]+|\d+[.)][ \t]+)"

After the fix:

  • Ctrl + S is no longer misidentified;
  • A + B is no longer misidentified;
  • x - y is no longer misidentified;
  • C++ is no longer misidentified;
  • - 第一项, + 第一项, and 1. 第一项 at the beginning of a line are still rejected.

At the same time, the GLM initial excerpt prompt also explicitly requires:

  • A continuous Chinese plain text paragraph;
  • No Markdown;
  • No bullet points and numbered lists;
  • No headings, quotes, tables, or code blocks;
  • No line breaks;
  • No prefixes like “Excerpt:”;
  • The content can be directly saved to the WordPress post_excerpt.

Complete test results:

Plaintext
334 passed
0 failed

3. Resuming to a Normal Run After excerpt_generated Caused Repeated Preflight Failures When the Excerpt Was Not Empty

A subsequent real drill with 20 articles strictly following the SOP in this article exposed another recovery branch issue. After the first execution, some articles stopped at execution=excerpt_generated: GLM had already generated the excerpt, and the excerpt had even been successfully written to WordPress, but subsequent GET, translation requests, or network connections were interrupted.

The old logic would remap both prepared and excerpt_generated to a normal run/restart. If the production excerpt had actually been saved, the next normal preflight would fail stably due to:

Plaintext
chinese_excerpt_empty: false
preflight_passed: false

and continuing to retry would just repeat the same error.

The final fix was kept very small: before retrying excerpt_generated, first confirm the actual state of the Chinese excerpt through the existing read-only production data source:

  • Excerpt saved: enter ready_for_translation_resume, automatically use resume to continue English overwrite translation;
  • Excerpt still empty: return to ready_for_execution, allowing GLM to be called again;
  • Production state temporarily unconfirmable: recorded as a retryable observation failure, without blindly executing run or resume.

After the fix, the remaining 4 real articles all completed on the first attempt of the next round; the complete test suite had 344 passes. The corresponding fix commit was b02bbf4.


13. Complete Check After Batch Completion

After batch execution finishes, you cannot only look at:

Plaintext
selected_count: 0

because it only means there are currently no articles in the ready_for_execution state, not necessarily that all are complete.

You must also execute:

Bash
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

When truly complete, the following should be met simultaneously:

Plaintext
selected_count: 0
allowed_count: 0
completed=20
remaining=0
excerpt_failed=0
translation_failed=0
next_action=Current batch completed

14. List Incomplete Articles and Failure Reasons

If you see:

Plaintext
completed=16
remaining=4

First execute read-only diagnostics:

Python
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


15. How to Handle Different Incomplete States

1. ready_for_execution

These articles are still handled by run-ready; do not switch to the generic resume. You can preview first:

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

If the article’s execution evidence is already excerpt_generated, the current code will automatically observe the production Chinese excerpt before actually retrying: if saved, it resumes translation; if still empty, it re-runs; there is no need to manually change the state to translation resume first.

2. translation_failed or ready_for_translation_resume

If the Chinese excerpt has been confirmed written, and the English overwrite translation needs to continue, use:

Bash
python3 bin/history-migration.py resume \
    --post-id 文章ID \
    --execute

This command only continues the English translation and does not regenerate the Chinese excerpt. However, you still need to check the real error before executing; deterministic structure or placeholder issues must be fixed in the translation pipeline first.

3. excerpt_failed

First check the most recently saved rejected excerpt:

Bash
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

Check whether the excerpt actually contains Markdown, lists, or other non-compliant formats, or if it was a false positive by the validator. Do not retry indefinitely without knowing the real model output.

If you need to directly re-execute the complete single-article flow after fixing the prompt or validator, you can execute:

Bash
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

Upon success, it will show:

Plaintext
{"chinese_post_id": Article ID, "english_post_id": English Article ID, "status": "completed"}

After directly invoking execute-single-candidate.py, if the execution evidence is complete but the coordination state has not synced, preview first:

Bash
python3 bin/history-migration.py sync-execution \
    --json

After confirming that only the target article is planned to be synced, execute:

Bash
python3 bin/history-migration.py sync-execution \
    --apply \
    --json

4. blocked / stale_execution_state

If status prompts that manual investigation is needed for blocked, first use show-current to find the target article, then read last_failure, execution_evidence, and workflow_status from that article’s state. Do not directly clear the state file.

If you confirm it is an orphaned attempt left from the run phase, and the execution evidence is excerpt_generated, you first need to confirm whether the production Chinese excerpt is actually empty or saved. Then do a preview first:

Bash
# 生产中文摘要仍为空
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

Only after the preview shows eligible=True and allowed=True, add --apply to the end of the same command. If the excerpt is empty, it will restore to ready_for_execution; if the excerpt is saved, it will restore to ready_for_translation_resume.

If the reason for blocked is not the aforementioned orphaned run attempt, investigate based on the real last_failure; do not apply this set of commands.


16. How to Determine If the Entire Batch Is Truly Complete

Finally execute:

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

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

When truly complete, the focus is not on a single command’s selected_count, but on the entire coordination state having converged:

Plaintext
completed = total in batch
ready = 0
in_progress = 0
translation_resume = 0
excerpt_failed = 0
translation_failed = 0
validation_failed = 0
blocked = 0
remaining = 0
integrity = ok

After completing this round of 20 real regressions on July 26, 2026, the repository status reached:

Plaintext
Fixed batches: 6
Fixed articles: 108
Execution evidence: completed=108 failed=0 pending=0 translation_started=0 other=0 no_execution_evidence=0
Integrity: ok conflicts=0 errors=0
Flow: ready=0 in_progress=0 translation_resume=0 excerpt_failed=0 translation_failed=0 validation_failed=0 blocked=0 remaining=0
Next step: Current batch completed
syntaxhighlighter-20260725-01: total=20 completed=20 remaining=0
Latest incomplete batch: None
Suggest creating next batch: True
Suggestion: all batches complete

retry_exhausted is a historical cumulative record of exhausted retries and does not mean there are still that many failed articles currently. In the final state of this round, even if it is still non-zero, as long as the current remaining=0 and all failed/blocked buckets are 0, it is complete.


17. Do Not Commit Local Running State

After batch processing, git status may show:

Plaintext
M data/state/history-migration/some-batch/chinese-articleID.json
M data/state/history-migration/some-batch/events.jsonl
?? data/state/history-migration/some-batch/

These are local running states and execution evidence.

You should:

  • Keep them;
  • Not commit them;
  • Not delete them;
  • Not execute git add .;
  • Not execute git add -A;
  • Not clear the state just to make the workspace look clean.

Content that needs to be kept includes:

Plaintext
data/state/
events.jsonl
data/backups/
*.execution.json
*.pre-write.json
rejected/

Code changes should be staged with exact file specifications, for example:

Bash
git add \
    src/candidate_execution.py \
    src/glm47_excerpt_client.py \
    tests/test_candidate_execution.py \
    tests/test_excerpt_clients.py

18. The Fixed Sequence to Truly Execute Every Day

Going forward, execute according to the fixed path below daily. The batch CSV and running state are kept locally, and the step “commit batch CSV” is no longer included:

Plaintext
1. Execute status and summary
2. Confirm the latest incomplete batch is None, and suggest creating next batch: True
3. Create a new fixed batch of 20 articles and execute init-state
4. Note down batch_id and csv_file; keep the batch CSV local, do not commit to Git
5. Output the list of 20 articles and backend edit URLs
6. Manually migrate SyntaxHighlighter; final requirement SH=0
7. Verify the language block by block for blocks migrated to Code Block Pro; CBP count is allowed to be less than the original SH count
8. Batch execute mark-converted
9. Check WP_ADMIN_COOKIE and WP_REST_NONCE; silently reload in the current terminal if missing
10. Batch execute validate-live
11. Directly retry network-type validation failures; use --refresh for validation_failed after manual fixes
12. run-ready preview, confirm all target articles are allowed
13. Check ZHIPU_API_KEY; silently reload if missing
14. Execute run-ready --execute, letting single articles retry a limited number of times and continue to subsequent articles
15. Execute status, summary, and show-current if necessary
16. ready_for_execution continues to be handled by run-ready
17. ready_for_translation_resume / translation_failed only use resume
18. For blocked, first verify the real state, and if necessary use reconcile-attempts preview before --apply
19. For excerpt_failed, first check the real model output or deterministic validation cause
20. Confirm completed=20, remaining=0, blocked=0, integrity=ok
21. Keep data/state, events, backup, and execution evidence; do not clean up local running state

The most critical principles are:

Plaintext
Do not treat resume as a unified recovery command for all incomplete articles.

Do not assume the batch is complete just because selected_count=0.

Do not automatically judge migration failure just because the Code Block Pro count is less than the original SyntaxHighlighter count.

Do not make the recovery logic more complex than actual needs just to save a few GLM tokens.

Do not blindly change states without checking the real error, real execution evidence, and actual production state.

19. Final Conclusion

The goal of this workflow is not to build the most theoretically complex automation system, but to stably complete WordPress historical article migration within acceptable time, cost, and maintenance effort.

The final working method is:

Plaintext
Repository selects and fixes articles
→ Manual selects the appropriate Gutenberg migration method based on content
→ Production read-only validation confirms SH=0, CBP does not exceed the original count, and checks other safety conditions
→ Repository batch generates excerpts and overwrites English translations
→ Single-article sporadic failures retry a limited number of times and continue to subsequent articles
→ excerpt_generated automatically selects resume or restart based on the production excerpt
→ Manual only intervenes in blocked or deterministic errors that truly require judgment
→ Only proceed to the next batch after completed=total in batch, remaining=0

Strictly following the article’s SOP to run through it again this time was very valuable, because the issues truly exposed were not “the batch processing framework needs a complete rewrite,” but rather a few very specific process gaps: reading the wrong command output when creating a batch, ignoring that the CSV in the directory should not be committed, overly strict migration count rules, missing recovery branches for validation failures, missing reload instructions for environment variables, and an incomplete recovery path for excerpt_generated.

After the last issue was fixed, the remaining 4 real articles all completed on the first attempt of the next round; the entire repository eventually reached 108/108 completed, and the latest 20-article batch reached 20/20 completed.

Therefore, there is no need to continue expanding into a more complex multi-round scheduling system. A more appropriate approach is to keep the current implementation simple: single-article limited retries, failure isolation, and recoverable state; execute daily only according to the fixed sequence in this article, and use the final remaining=0 as the completion standard.

系列导航

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

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