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

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 some waiting time. When processing 20 articles a day, I constantly switch back and forth between the WordPress admin dashboard, the terminal, and translation statuses.

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

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

This post documents how this workflow came together and serves as an operational manual that I can directly follow every day going forward.


1. What Each of the Two Repositories Handles

This workflow involves two independent repositories.

1. WordPress Translation Pipeline Repository

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

It is primarily responsible for:

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

2. Historical Article Excerpt Backfill Repository

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

It is primarily responsible for:

  • Selecting historical articles to process;
  • Creating fixed batches;
  • Saving the relationship between Chinese and English articles;
  • Generating Chinese excerpts;
  • Triggering production environment overwrite translation;
  • Saving pre-write backups;
  • Recording execution status;
  • Batch retries 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 Work and the Repository

Repository Responsibilities

The repository is responsible for:

  • Selecting Chinese articles with empty excerpts;
  • Excluding articles already assigned to previous batches;
  • Confirming that a published English translation exists for the article;
  • Fixing the 20 articles to be processed each day;
  • Saving the Chinese article ID and English article ID;
  • Saving the admin editor URLs;
  • Recording the original SyntaxHighlighter count;
  • Calculating the expected Code Block Pro count after migration;
  • Executing production read-only validation;
  • Calling GLM to generate Chinese excerpts;
  • Writing the Chinese excerpts;
  • Calling SlyTranslate to overwrite the English articles;
  • Saving execution evidence and backups;
  • Performing limited retries for single-article failures;
  • Summarizing completed, failed, and remaining counts.

Manual Responsibilities

Manual work is only responsible for:

  • Opening articles based on the repository list;
  • Converting SyntaxHighlighter to Code Block Pro;
  • 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 work is no longer responsible for:

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

The final boundary of responsibilities can be summarized as:

Plaintext
The repository decides which articles to process
→ Manual work completes the code block migration that requires human judgment
→ The 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 most easily overlooked step is:

Plaintext
mark-converted

Even if the code block conversion is already finished in the WordPress admin dashboard, the repository will not know this automatically.

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 the 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 the next batch

If you see:

Plaintext
建议创建下一批: False
建议: 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
最新未完成批次: 无
建议创建下一批: True
建议: 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 in a normal terminal:

Bash
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

On 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 require them.

The new batch CSV is a fixed list and can be committed separately:

Bash
git add "$batch_file"
git diff --cached --check
git commit -m "chore: 固定 ${batch_id} 批次"
git push origin main

Do not execute:

Bash
git add .
git add -A

to avoid committing local runtime states like data/state.


6. View the Article List and Admin Editor 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 display:

Plaintext
批次总数:20
当前待处理:20
Figure 2: The repository lists the 20 articles for the day, their code block counts, and admin editor URLs.
Figure 2: The repository lists the 20 articles for the day, their code block counts, and admin editor URLs.

7. Manually Migrate SyntaxHighlighter

Open the Chinese articles one by one using the admin URLs from the list.

Each article requires:

  1. Converting all SyntaxHighlighter blocks to Code Block Pro;
  2. Checking if the code content is complete;
  3. Verifying the code language one by one;
  4. Checking Gutenberg for broken blocks;
  5. Saving the article.

When the original block explicitly declares a language

Set it to the corresponding language, for example:

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

Both the .yml and .yaml files should select:

Plaintext
YAML

When the original block does not declare a language

Set it to:

Plaintext
Plaintext

Code Block Pro sometimes inherits the language used in the previous block, so you must verify each block before saving.

Before saving each article, confirm that:

Plaintext
SyntaxHighlighter count is zero
Code Block Pro count matches the expected count from the batch list
No code content is missing
Code language is set correctly
Gutenberg shows no broken block warnings
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, verifying the code languages one by one.
Figure 4: After converting to Code Block Pro, verifying the code languages one by one.

8. Record That Manual Conversion Is Complete

After successfully saving in the WordPress admin dashboard, 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 display:

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

This step only updates the local state and will not:

  • Call GLM;
  • Call 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 and only supports:

Plaintext
--post-id

It does not support:

Plaintext
--batch-id

Therefore, you need to read the article IDs from the batch CSV and execute the validation 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

Then execute:

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, it should display:

Plaintext
========== 只读验证最终汇总 ==========
本次待验证:20
本次通过:20
本次失败:0
批次当前 ready:20
批次已经 completed:0

And:

Plaintext
模式: preview
完整性: ok
selected_count: 20
allowed_count: 20
Figure 5: 20 articles have completed production read-only validation and all entered the ready state.
Figure 5: 20 articles have completed production read-only validation and all entered the ready state.

10. Batch Generate Excerpts and Overwrite English Translations

The formal execution requires three environment variables:

Plaintext
WP_ADMIN_COOKIE
WP_REST_NONCE
ZHIPU_API_KEY

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, the results for each article are displayed in real time.

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 performs a limited number of automatic attempts for each individual article.

It is suitable for resolving:

  • Temporary request timeouts;
  • Brief network failures;
  • Intermittent HTTP 502 or 503 errors;
  • Short-term server unavailability;
  • One-off model response anomalies.

However, it cannot automatically resolve:

  • Conflicts between prompts and validation rules;
  • Overly strict Plaintext structure rules;
  • Missing placeholders;
  • Corrupted Gutenberg structure;
  • Validation regex false positives;
  • The model consistently returning the same non-compliant format.

Therefore, if an article still fails after three retries, you should not blindly increase the retry count.

The correct workflow should be:

Plaintext
Automatic limited retries
→ Output the final failure reason
→ Determine if it is a transient fault or a deterministic fault
→ Only continue resume for transient faults
→ Fix the root cause first for deterministic faults

12. Two Real Issues Exposed in This Batch

After the first batch execution of these 20 articles:

Plaintext
16 completed
3 translation_failed
1 excerpt_failed

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

Upon further inspection, I found that the 4 failures were not due to random network issues, but rather two deterministic rule problems.

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 standard Plaintext content, the physical line count before and after translation should not be required to match exactly.

The final fix was to:

  • Remove the line-by-line equal length requirement for translatable Plaintext;
  • Preserve the START/STOP region boundaries;
  • Preserve token count, uniqueness, numbering, and order validation;
  • Non-empty Plaintext must not become entirely empty;
  • Plaintext must not absorb adjacent Gutenberg or protected regions;
  • Untranslatable content such as code, commands, paths, URLs, and status codes remain 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 项通过
0 项失败

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 Falsely Identified as a Markdown List

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

Plaintext
在 VS Code 中按下 Ctrl + S 时文件内容意外被还原……

However, 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.

As a result, the:

Plaintext
Ctrl + S

within it:

 + 

was falsely identified as a Markdown + bullet point.

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

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

After the fix:

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

At the same time, the initial GLM 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 “Summary:”;
  • The content can be saved directly to the WordPress post_excerpt.

Complete test results:

Plaintext
334 项通过
0 项失败

13. Complete Check After Batch Completion

After batch execution finishes, you cannot just look at:

Plaintext
selected_count: 0

because it only means there are currently no articles in the ready_for_execution state, which does not necessarily mean everything is 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 must be met simultaneously:

Plaintext
selected_count: 0
allowed_count: 0
completed=20
remaining=0
excerpt_failed=0
translation_failed=0
next_action=当前批次已完成

14. List Incomplete Articles and Failure Reasons

If you see:

Plaintext
completed=16
remaining=4

First execute a read-only diagnostic:

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

1. translation_failed

If the Chinese excerpt has been written but the English overwrite translation failed, use:

Python
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, before executing, you must look at the actual error.

If it is a temporary network error, you can directly resume.

If it is:

Plaintext
Plaintext line count changed
占位符遗漏
Gutenberg 结构损坏

or other deterministic errors, you should fix the translation pipeline first, then execute resume.

2. 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 or a list, or if it was falsely flagged by the validator.

Do not continue to retry indefinitely without knowing the actual content.

3. Re-run the Single-Article Complete Workflow After Fixing

excerpt_failed cannot use resume.

After fixing the prompt or validator, 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

On success, it will display:

JSON
{"chinese_post_id": 文章ID, "english_post_id": 英文文章ID, "status": "completed"}

4. Sync Single-Article Execution Evidence

After directly calling execute-single-candidate.py, the execution evidence might already be completed, but the coordination state still retains the old excerpt_failed.

First, preview:

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

After confirming that it only plans to sync the target article, execute:

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

This step only updates the local coordination state; it will not call GLM again, nor will it modify WordPress again.


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

The actual results after successful completion this time were:

Plaintext
固定文章: 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

The current batch of 20 articles shows:

Plaintext
total=20
completed=20
remaining=0
next_action=当前批次已完成

Note that:

Plaintext
retry_exhausted=9

is a historical cumulative record of exhausted retries, and does not mean there are still 9 failed articles currently.

To determine if the current batch is complete, you should look at:

Plaintext
excerpt_failed
translation_failed
blocked
remaining

rather than just the cumulative retry_exhausted.


17. Do Not Commit Local Runtime State

After batch processing, git status might show:

Plaintext
M data/state/history-migration/某批次/chinese-文章ID.json
M data/state/history-migration/某批次/events.jsonl
?? data/state/history-migration/某批次/

These are local runtime 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 working directory 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 stage files precisely, 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 Execute Every Day

From now on, process in the following order every day:

Plaintext
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. 保留本地运行状态

The most critical principles are:

Plaintext
不要因为有自动重试,就把所有失败都视为网络波动。

不要因为 selected_count=0,就认定批次已经完成。

不要在没有查看真实错误和真实模型输出前继续盲目重试。

19. Final Conclusion

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

The final working method is:

Plaintext
The repository selects articles
→ Manual work handles code blocks
→ The repository completes validation, excerpts, and English overwrite translation
→ Automatic retries handle intermittent faults
→ Manual work only handles true deterministic anomalies

All 20 articles in this batch were ultimately completed, which also verified several important conclusions:

  1. Limited retries can resolve intermittent issues, but cannot replace accurate error classification;
  2. When translating from Chinese to English, a change in the physical line count of Plaintext is a reasonable phenomenon;
  3. The initial prompt should align completely with local validation from the very first request;
  4. Validation rules must be tested with real technical text to avoid falsely flagging expressions like Ctrl + S as Markdown;
  5. After a direct single-article execution succeeds, the coordination state must still be synced;
  6. Only when completed equals the total batch size and remaining=0 is the batch truly complete.

Following these fixes, new batches starting tomorrow can directly follow this workflow without needing to scramble for commands or figure out how to handle failed articles from scratch.

系列导航

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

我是拥有 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 来减少垃圾评论。了解你的评论数据如何被处理