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

Complete SOP for Daily Batch Processing of 20 Mixed WordPress Legacy Posts: Classic to Gutenberg Conversion, SyntaxHighlighter Migration, Excerpt Completion, and English Override Translation

Complete SOP for Daily Batch Processing of 20 Mixed WordPress Legacy Posts: Classic to Gutenberg Conversion, SyntaxHighlighter Migration, Excerpt Completion, and English Override Translation

作者:

In the previous post, I organized “Batch Processing 20 WordPress Legacy Articles Daily: A Complete SOP for Code Block Migration, Summary Completion, and English Override Translation”.

That phase primarily dealt with legacy articles that were already in Gutenberg format but still contained SyntaxHighlighter in the body. After continuing the cleanup, I found that the site still had quite a few Chinese legacy articles containing SyntaxHighlighter; upon further inspection, I confirmed that these were not missed by the previous scan, but belonged to another legacy format category: the body simultaneously contained Gutenberg blocks, Classic / Classic editor content, and SyntaxHighlighter, which the parser identifies as editor_format=mixed.

The earlier Gutenberg + SyntaxHighlighter phase only handled editor_format=gutenberg articles, so these Mixed articles were naturally left for the next phase. There were 717 Mixed candidates in the historical snapshot, of which 712 primarily suffered from editor-format-mixed; the remaining 5 also had other code formats or structural anomalies and were temporarily set aside.

Therefore, the goal of the second phase was clear: first normalize Classic / Mixed content into Gutenberg, then migrate SyntaxHighlighter to Code Block Pro. After completing the production read-only validation, the repository would automatically generate the Chinese summary and override the English translation.


1. Why the Mixed Phase Requires Separate Handling

The biggest difference between Mixed articles and those in the previous phase is not SyntaxHighlighter itself, but the fact that the entire article has not yet truly become standard Gutenberg. Even if some Gutenberg blocks are already visible in the backend, Classic content may still exist outside the block structure.

Therefore, the Mixed phase cannot simply perform “SyntaxHighlighter → Code Block Pro”. Before entering automatic summary and English override translation, the following must also be satisfied: Classic / Mixed content has completed Gutenberg normalization, the layout after automatic conversion has been manually checked, SyntaxHighlighter has been reduced to zero, the Code Block Pro count and languages are correct, and production read-only validation confirms editor_format=gutenberg, classic_outside_blocks=false.

In other words, “Classic blocks have disappeared” is not directly equivalent to “Gutenberg normalization is complete”. After a one-click conversion of complex Classic content, issues such as paragraph merging, headings turning into normal paragraphs, numbering disorder, loss of line breaks, or changes in image or caption positions may still occur, which still require manual inspection.

2. Two Repositories and Manual Responsibilities

1. WordPress Translation Pipeline

The repository path is /home/wangqiang/code/wordpress-ai-translation-pipeline, primarily responsible for SlyTranslate and GLM translation customization, Gutenberg structure protection, HTML / code / shortcode protection, placeholder validation, and full English override translation.

2. Legacy Article Migration Repository

The repository path is /home/wangqiang/code/wordpress-ai-excerpt-backfill, primarily responsible for candidate screening, fixed batches, manual conversion status, production read-only validation, Chinese summary generation, English override translation scheduling, execution evidence, failure recovery, and batch summarization.

3. What Manual Work Actually Needs to Be Done

  • Open the article specified by the repository, and first determine whether the current body is already standard Gutenberg;
  • If it is still Classic, Mixed, or unknown, normalize the body into Gutenberg, and check the paragraphs, headings, numbering, lists, and line breaks after conversion;
  • If SyntaxHighlighter still exists, migrate it to Code Block Pro and verify the language block by block; if there is no legacy code structure to begin with, do not modify the body just for the sake of the workflow;
  • Check images, captions, links, body order, and whether there are any broken blocks;
  • Confirm that the current article meets the production validation requirements, and finally batch record that manual inspection or conversion is complete.

In a normal batch, manual intervention is not required to find the next batch of articles, nor to manually generate Chinese summaries or click through English override translations one by one. Only when the automated model cannot complete the task due to deterministic reasons such as content safety filtering, and unified recovery can no longer safely proceed through the normal automated path, do we enter the ChatGPT manual fallback process described later.


3. Complete State Transitions

The normal main flow still uses the state machine from the previous phase:

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

What is truly new in the Mixed phase is the manual confirmation that “full Gutenberg normalization is complete”, along with the production validation checks for editor_format=gutenberg, classic_outside_blocks=false.

Upon reaching the remaining empty-summary articles phase, this state machine remains unchanged. What changes is that the candidate entry has been relaxed: articles no longer need to first belong to the Mixed category or contain SyntaxHighlighter; however, before entering ready_for_execution, they must still pass the original Gutenberg, Classic residual, SyntaxHighlighter, code structure, and production safety validations.

The production validation phase still needs to distinguish between awaiting_readonly_validation and validation_failed; single-article failures during the formal execution phase are uniformly handed over to recover. The program automatically selects a safe recovery strategy based on the local state, execution evidence, pre-write baseline, and production Chinese source, which is detailed separately later.


4. Check Repository Status Before Starting Each Day

Before starting operations each day, first enter the repository and check the global status:

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

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

Focus on “latest unfinished batch”, “next step”, and “suggest creating next batch”. If a latest unfinished batch still exists, continue completing the current batch instead of prematurely creating a new 20-article batch. Only create the next batch when you see “latest unfinished batch: none”, “suggest creating next batch: True”, and “suggestion: all batches complete”.

5. Create a New Fixed Batch of 20 Articles

August 12, 2026: Candidate Entry Expanded to All Remaining Empty-Summary Articles

The initial Mixed + SyntaxHighlighter phase used bin/build-mixed-syntaxhighlighter-batch.py to create fixed batches. By August 12, 2026, this phase and the last 5 anomalous articles were all completed, and the legacy migration status reached 45 fixed batches with all 857 articles completed.

Subsequently, a read-only inventory of the production environment was performed again, confirming that 424 published Chinese legacy articles still met the criteria of “empty Chinese summary, existing Polylang English counterpart, English article is published, and not yet in an existing fixed batch”. Therefore, the subsequent phase no longer uses Mixed, SyntaxHighlighter, or any specific code format as the candidate entry. Instead, it continues to use the existing batch generator, simply relaxing the candidate criteria to cover all remaining empty-summary legacy articles.

The batch sorting rules remain unchanged, still strictly sorted by published_at DESC, chinese_post_id DESC, meaning we process from historically newer articles to older ones; each batch contains a maximum of 20 articles, and if the remaining candidates are fewer than 20, all remaining candidates are processed directly.

The script and batch names currently still retain mixed-syntaxhighlighter to maintain compatibility with the already stable fixed batches, state directories, and history-migration workflows; this does not mean that candidates in the new phase must still contain SyntaxHighlighter.

After confirming that the previous batch is complete, you can execute:

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
        raw_file="data/raw/history-general-candidates-20260812-01.jsonl"
        preview_file="data/analysis/history-general-candidates-20260812-01-preview.csv"
        translations_file="data/raw/history-general-candidates-20260812-01-translations.jsonl"

        if [ ! -f "$raw_file" ]; then
            echo "找不到原始候选快照:$raw_file"
        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"

            if python3 bin/build-mixed-syntaxhighlighter-batch.py \
                --preview "$preview_file" \
                --translations "$translations_file" \
                --output "$batch_file" \
                --batch-id "$batch_id" \
                --maximum 20 \
                "$raw_file"
            then
                if [ -s "$batch_file" ] && [ "$(wc -l < "$batch_file")" -gt 1 ]; then
                    python3 bin/history-migration.py init-state --apply \
                    && echo \
                    && echo "新批次已经创建:" \
                    && echo "batch_id=\"$batch_id\"" \
                    && echo "csv_file=\"$batch_file\""
                else
                    echo
                    echo "没有创建新批次。"
                    echo "请检查上面的 selected_count 和 remaining_eligible_count。"
                fi
            else
                echo
                echo "创建新批次失败,未执行 init-state。"
            fi
        fi
    fi
fi

Here, it still first checks whether summary allows creating the next batch; only when the batch CSV is actually generated and contains at least one candidate record does it execute init-state --apply and output “new batch created”. This prevents falsely reporting a successful creation when the candidate pool is already exhausted.

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

6. Set Batch Variables and Output Backend Edit URLs

Each day, you only need to separately set the two variables that change, for example:

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

Then output the backend edit URLs for all unfinished articles in the current batch at once:

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

7. Manual Inspection and Normalization to Gutenberg

1. First Determine Whether the Article Actually Needs Modification

Upon entering the remaining empty-summary articles phase, not every article requires body modifications. The read-only inventory on August 12, 2026, showed that out of 424 official candidates, 74 were already standard Gutenberg with no legacy code formats needing migration; such articles only require manual confirmation that the structure and layout are normal, without needing to save or rewrite the body again for the migration workflow.

2. Normalize Classic / Mixed / unknown to Gutenberg

If the article still belongs to Classic, Mixed, or unknown, continue normalizing it to Gutenberg according to the previous rules. Simple Classic content can directly use WordPress’s “Convert to Blocks”; complex content must still be manually checked for layout after conversion.

Before saving, at least confirm: paragraphs, headings, numbering, lists, line breaks, images, captions, links, and body order are normal; there is no missing, duplicated content or broken blocks. Ultimately, it must be able to pass the existing production validation checks for conditions like editor_format=gutenberg, classic_outside_blocks=false.

3. Legacy Code Structures Still Handled According to Existing Rules

If legacy code structures are found, continue processing them according to the existing validation rules. SyntaxHighlighter must ultimately be 0; if SyntaxHighlighter still exists, migrate it to Code Block Pro and verify the language block by block. If the original block already declares a language, continue using that language; if no language is declared, prioritize using Plaintext, and do not let Code Block Pro inadvertently inherit the language of the previous code block.

core/code, classic pre/code, or other anomalous structures prohibited by the existing validator must also be normalized before entering production read-only validation. In the read-only inventory of the current 424 remaining candidates, the SyntaxHighlighter count is already 0; retaining this hard validation ensures that the SyntaxHighlighter plugin can be safely deleted after all legacy articles have been processed.

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

8. Batch Record Manual Conversion Completion

After all articles have completed the necessary manual inspection or conversion, the status needs to be advanced from awaiting_manual_conversion to awaiting_readonly_validation. For articles that were already clean Gutenberg, this confirmation indicates that a manual check has been performed, not that the body had to be modified. First confirm $batch_id and $csv_file, then execute:

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

Under normal circumstances, the summary should show that all articles pending recording in this batch were successful, with 0 failures.


9. Execute Production Read-Only Validation

1. Obtain Cookie and REST Nonce from the Browser

Production read-only validation requires using the currently logged-in WordPress backend session. WP_ADMIN_COOKIE stores the complete Cookie value from the backend request, and WP_REST_NONCE stores the X-WP-Nonce value from the same login session.

You can obtain these in the browser as follows:

  • Log in to the currently used WordPress admin backend, and open any article edit page;
  • Open the browser developer tools, and switch to the “Network” tab;
  • Refresh the edit page, or perform a backend operation that does not damage content;
  • Filter the network requests for wp-json, and open a WordPress REST API request with a 200 status;
  • In the request headers, copy the full value of Cookie as WP_ADMIN_COOKIE;
  • In the request headers of the same request, copy the value of X-WP-Nonce as WP_REST_NONCE.

When copying, only take the value after the colon in the request header; do not copy the request header names Cookie: or X-WP-Nonce:. Both items should ideally come from the same successful request to avoid the Cookie and Nonce belonging to different login sessions.

Cookies and REST Nonces are sensitive authentication credentials and should not be written into blogs, screenshots, Git repositories, script source code, or commit logs.

2. Set Environment Variables in the Current Terminal

Do not directly execute export WP_ADMIN_COOKIE='...' with real values, as the content may enter the shell history. You can use silent input; the terminal will not display the actual content when pasting:

Bash
if [ -z "${WP_ADMIN_COOKIE-}" ]; then
    read -r -s -p "请输入新的 WordPress Cookie:" WP_ADMIN_COOKIE
    echo
    export WP_ADMIN_COOKIE
fi

if [ -z "${WP_REST_NONCE-}" ]; then
    read -r -s -p "请输入新的 X-WP-Nonce:" WP_REST_NONCE
    echo
    export WP_REST_NONCE
fi

Here, export only makes the variable effective in the current shell / terminal session and its child processes. After closing the terminal, reopening a new VS Code Terminal, or switching to another terminal tab, you usually need to set it again. The current workflow does not write these sensitive values to .env, ~/.bashrc, or other persistent files.

3. Check Variables and Identify Expired Credentials

After setting them, you can just check whether the variables are non-empty without printing the actual content:

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

Note: “is set” here only means the variable is non-empty, not that the Cookie or REST Nonce is still valid. During actual execution on July 31, 2026, I encountered a situation where WP_REST_NONCE already existed, but actual requests kept returning HTTP 403; after retrieving and setting a new Nonce, the same batch immediately returned to normal.

Therefore, if multiple articles consecutively return 403 at the same REST preflight position, you should not primarily suspect the article content, but should first check the current credentials, especially WP_REST_NONCE. When an update is needed, you can directly re-enter the silent input and overwrite the old value:

Bash
read -r -s -p "请输入新的 WordPress Cookie:" WP_ADMIN_COOKIE
echo
export WP_ADMIN_COOKIE

read -r -s -p "请输入新的 X-WP-Nonce:" WP_REST_NONCE
echo
export WP_REST_NONCE

If you have confirmed that the Cookie is still valid, you can also just reset WP_REST_NONCE; if unsure, it is best to re-copy both values from the same new successful REST request.

4. Batch Production Read-Only Validation with Limited Retries for Sporadic SSH Timeouts

Upon entering the remaining empty-summary articles phase, production read-only validation continues to follow the original structure and production safety checks. Before formal execution, the current Chinese body must have become standard Gutenberg, classic_outside_blocks=false, SyntaxHighlighter=0, and passed existing checks such as Code Block Pro, broken blocks, Polylang, publish status, SHA-256, and target drift. Although the candidate entry has been relaxed, the execution validation standards have not. In actual operation, if batch read-only SSH query timed out or SSH exit 255 occurs, and the status remains at awaiting_readonly_validation, it usually means the read-only query did not complete successfully, and a limited retry can be performed.

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

If the article has truly entered validation_failed, do not treat it as a sporadic network timeout. You should fix the Chinese article first, and then use validate-live --post-id 文章ID --refresh to re-validate.


10. Formally Generate Chinese Summary and Override English Translation

1. Set Zhipu API Key

Production read-only validation only requires WP_ADMIN_COOKIE and WP_REST_NONCE. Formally generating the Chinese summary also requires the project’s existing Zhipu API Key, which is ZHIPU_API_KEY.

Again, do not write the real API Key directly into the command history. You can silently input it in the current terminal:

Bash
if [ -z "${ZHIPU_API_KEY-}" ]; then
    read -r -s -p "请输入智谱 API Key:" ZHIPU_API_KEY
    echo
    export ZHIPU_API_KEY
fi

ZHIPU_API_KEY also only takes effect in the current terminal session, and should not be written into blogs, screenshots, Git repositories, or public configuration files.

Before formal execution, uniformly check the three variables:

Bash
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 "环境变量不完整,暂不执行正式批次。"
fi

This check still only verifies whether the variables are non-empty. Whether the Cookie and REST Nonce are valid depends on the actual REST preflight results; if multiple articles simultaneously return 403, you should update the credentials first instead of continuing to consume single-article retry attempts.

2. Formally Execute the Batch

After confirming that all three variables are set and the current credentials are valid, execute:

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

The program will execute Chinese summary generation, write verification, and English override translation article by article. Genuine timeouts, DNS failures, connection interruptions, and brief service unavailability are still handled by the limited retry mechanism, so you do not need to immediately abort the entire batch when seeing a transient network error.

However, HTTP 500 can no longer be universally treated as a retryable server-side fluctuation. The program will now parse the JSON code and message returned by WordPress: for example, swq_full_article_token_validation_failed will be classified as protected_token_validation_error, and execution will stop immediately after the first failure. Authentication-related 403s must also be treated differently; when multiple articles consecutively return 403 at the same REST preflight position, you should update WP_ADMIN_COOKIE or WP_REST_NONCE first, rather than continuing to consume single-article retry attempts.

11. How to Determine True Completion After the Batch Ends

After formal execution ends, execute again:

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

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

True completion cannot be determined solely by selected_count=0. It only means that there are currently no articles that can be directly executed by run-ready, and does not mean that translation_failed, validation_failed, or blocked do not exist.

Ultimately, the following must be simultaneously satisfied: the current batch completed=批次总数, remaining=0, no failure statuses, integrity=ok, and the global output shows “latest unfinished batch: none” and “suggest creating next batch: True”.


12. Abnormal Statuses and Error Classification

Starting August 4, 2026, anomaly recovery no longer requires manually determining whether to use resume or restart-from-current. However, before entering the unified recovery command, it is still necessary to distinguish between “validation phase failure” and “formal execution phase failure”, because the evidence handled and the safety boundaries differ between the two.

1. awaiting_readonly_validation + SSH timeout

If the production read-only query reports batch read-only SSH query timed out, and the article status is still awaiting_readonly_validation, it means the validation has not yet reached a success or failure conclusion, and a limited retry can be performed for articles still in this state.

SSH exit code 255 will now attempt up to three times at the underlying level with brief backoff. If all three attempts fail, it should report production_readonly_unavailable and “content change unknown”; during this time, no status must be written, and changed=False must not be mistakenly treated as “Chinese source has not changed”.

2. validation_failed

This indicates that the production read-only validation has reached a definitive conclusion, and is not a sporadic network error. You should first fix the WordPress Chinese article, and then execute:

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

Only after re-validation passes and enters ready_for_execution should formal execution continue.

3. Formal Execution Failures Must Differentiate Between Transient and Deterministic Errors

Genuine transient errors may still occur during the formal execution phase, such as GLM request timeouts, DNS failures, connection interruptions, or brief service unavailability. Such errors continue to use limited retries, and the batch will not immediately abort due to a single sporadic failure.

However, HTTP 500 itself can no longer be directly equated to a network failure. The program will now continue to parse the JSON code, message, and data returned by WordPress. For example, swq_full_article_token_validation_failed will be classified as protected_token_validation_error, which is a deterministic content or protection token validation error, and will stop after the first failure instead of automatically retrying three times meaninglessly.

The batch output and status files will retain the real error category, WordPress error message, and execution evidence path. When encountering such errors, you should first review the error message and fix the corresponding Chinese source, and then use the unified recover command below.

4. Summary Phase HTTP 400 / 1301 Content Safety Filtering

On August 14, 2026, another type of summary phase error that should not be blindly retried was added: the Zhipu API returns HTTP 400, but the actual response contains error.code=1301, indicating that the input or generated content may contain unsafe or sensitive content. This type of failure is not a normal network fluctuation, nor can it be guessed as a request length or parameter issue based solely on HTTP 400.

The HTTP JSON layer will now identify both top-level and nested error.code / error.message, and the execution evidence will also retain the desensitized HTTP status, error response, and request size statistics. You need to look at the actual evidence first: if it is clearly a 1301 content safety filter, stop making meaningless repeated requests; if the latest occurrence is just a TimeoutError, it should still be recorded separately from the original content filtering root cause, and they must not overwrite each other.

For this kind of deterministic model rejection, if the article itself does not contain content that needs modification, you should not rewrite the legacy article just to accommodate a particular model. A ChatGPT manual fallback process has been added below, using external manual work to complete the Chinese summary and English translation, and then converging to completed through read-only acceptance and mark-manual-completed.

5. Multiple Articles Simultaneously Returning 403

If multiple articles consecutively return 403 at the same REST preflight position, you should prioritize checking WP_ADMIN_COOKIE and WP_REST_NONCE. This is a failure of a shared authentication dependency, not a simultaneous content anomaly across multiple articles, and you should not continue to consume single-article retry attempts.


13. Unified Use of recover to Restore Single Failed Articles

Previously, the recovery process was scattered across resume, restart-from-current --apply, and run-ready --execute, requiring manual reading of the status, comparing the Chinese source, and selecting commands. Now, the repository has added a unified recovery entry, where the program automatically selects a safe strategy based on the existing status, execution evidence, pre-write baseline, and production read-only data. This unified recovery primarily targets single articles that have formed stable failure evidence; if local execution is interrupted by Ctrl+C, and the status stops at translation_started / execution_in_progress, you must first complete the coordination of the orphaned attempt and execution evidence before returning to the normal recovery path.

1. First Preview Recovery Strategy in Read-Only Mode

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

python3 bin/history-migration.py recover \
    --post-id 文章ID

Preview mode does not write to the status, nor does it re-execute the article. It displays the current status, real error, execution evidence, whether the production Chinese source has changed, recovery strategy, steps to be executed, and the next step.

2. Confirm, Then Automatically Recover and Execute

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

recover --execute completes the necessary recovery steps in a single call, processing only the target article, and will not re-execute other articles in the same batch that are already completed. Under normal circumstances, there is no longer a need to subsequently manually run run-ready --execute for the entire batch.

3. Strategies recover Will Automatically Select

  • completed → none: The article is already complete; it safely returns immediately without connecting to SSH, re-executing, or writing to the status, displaying “Next step: none”;
  • translation_failed + 中文源未修改 → resume: Reuses the existing safety baseline and continues execution from the point of failure;
  • translation_failed + 中文标题或正文已修改 → restart_from_current: Archives the old execution / pre-write data, rebuilds the baseline with the current production version, and re-executes the target article in the same call;
  • excerpt_failed + 中文源未修改 → retry_excerpt_generation: Summary generation failed before writing to WordPress; if the Chinese source, empty summary, and English baseline still match, it only regenerates the Chinese summary and continues safe execution; if the evidence clearly shows a deterministic content safety filter like 1301, do not continue blind retries, and switch to the ChatGPT manual fallback later in this section;
  • production_readonly_unavailable → blocked: Failed to obtain production data, cannot determine whether the Chinese source has changed, and does not write any status;
  • Conditions that do not meet existing safety checks → blocked: Explicitly prints the reason, and does not bypass checks through an unconditional reset.

The original resume, restart-from-current --apply, and other commands are retained for compatibility and specialized troubleshooting. Normal daily failures still prioritize using recover; only translation_started / execution_in_progress left by interruptions, orphaned attempts, or count drift status coordination issues enter the specialized recovery process below.

4. Real Case: Protected Token Validation Failure for 6965

In batch mixed-syntaxhighlighter-20260804-02, 19 articles completed at once, while Chinese article 6965 consecutively returned HTTP 500 during the English override translation phase. The old classification marked it as transient_network_error, triggering three retries within the batch, followed by another failure in the single-article resume.

Only after reading data/backups/single-candidate/chinese-6965.execution.json was the real error confirmed to be swq_full_article_token_validation_failed: 23 protection tokens were expected, but 27 were actually received, with 4 extra SWQINLINE...END appearing. This was not a network fluctuation, but a deterministic duplicate output of inline protection tokens by GLM-5.2.

After manually simplifying the technical strings in the Chinese title, normal paragraphs, and image alt text that easily triggered inline protection, the unified recovery process should only require:

Bash
python3 bin/history-migration.py recover \
    --post-id 6965

python3 bin/history-migration.py recover \
    --post-id 6965 \
    --execute

The program will detect that the Chinese source has changed, automatically select restart_from_current, rebuild the baseline, and execute only 6965. The article ultimately completed in one go, and the batch recovered to all 20 articles completed.

This case also drove fixes to the error classification: from then on, protected_token_validation_error stops immediately after the first failure, and directly displays the real code, message, and evidence path, instead of disguising it as a normal HTTP 500 network error.

5. The completed Status Must Be a Safe No-Op

The first real smoke test after the unified recovery command went online also revealed: even if the article was already completed, the old implementation would still continue querying the production environment; if SSH happened to be unavailable, it would erroneously display the completed article as blocked.

The judgment order has now been adjusted to read the local workflow status first. Upon encountering completed, it immediately returns strategy=none, without querying the production environment, comparing the Chinese source, rebuilding the baseline, re-executing, or modifying the execution evidence.

Plaintext
Mode: execute
Article: zh=6965 en=13099
Current status: completed
Real error: none
Production Chinese source: no need to check
Recovery strategy: none
Will execute: none
Write operation: no
Reason: Article is already complete, no recovery needed
Next step: none

6. Stopping at translation_started / execution_in_progress After Ctrl+C Interruption

During real recovery on August 8, 2026, another edge case beyond the unified recover was added: pressing Ctrl+C after resume --execute had already sent a remote request will cause the local Python process to exit, but the remote request sent to WordPress / GLM is not guaranteed to be synchronously canceled. Therefore, Ctrl+C cannot be interpreted as “this attempt never happened”.

If you subsequently see that the workflow is still execution_in_progress, the execution evidence is translation_started, and directly executing resume returns selected_count: 0, do not delete the status file, and do not manually modify the JSON. First execute show-current --json to verify the current batch and real evidence, and then perform an attempt reconciliation for the resume phase where the interruption occurred.

Bash
python3 bin/history-migration.py show-current --json

python3 bin/history-migration.py reconcile-attempts \
    --post-id 文章ID \
    --stage resume \
    --json

Look at the preview first. Only after confirming that eligible=true and planned_count meet expectations, and that the target is indeed this article, do you add --apply:

Bash
python3 bin/history-migration.py reconcile-attempts \
    --post-id 文章ID \
    --stage resume \
    --apply \
    --json

reconcile-attempts is responsible for aligning the started / terminated / orphaned attempts with the current recovery generation. After the fix on August 8, 2026, these statistics are only calculated within the current recovery_generation: attempts from old generations are retained for full lifecycle auditing, but no longer consume the retry quota of the new generation.

If the old logic has already erroneously written historical attempts into the current retry_counts, even if there are no orphaned attempts, the preview may still show counter_drift=true and reconciliation_action=counter_drift_correction. In this case, apply only corrects the count for the current generation, without changing lifetime_retry_counts, execution evidence, or other articles.

After attempt reconciliation is complete, preview the synchronization of the execution evidence and coordination state:

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

Only after confirming that the articles planned for synchronization in items meet expectations—for example, recovering from blocked / execution_in_progress to ready_for_translation_resume—do you execute:

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

Subsequently, you must first perform a resume Preview, rather than directly sending model requests again:

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

Only after seeing selected_count: 1, allowed_count: 1 do you execute:

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

If this step returns 403 rest_cookie_invalid_nonce during the WordPress REST preflight phase, update the Cookie / REST Nonce first, and then continue recovery, instead of retrying consecutively. A failed formal execution may also be recorded in the current generation’s retry count; when authentication has clearly expired, continuing to retry only wastes retry quota.

This specialized process is only for situations where “a local interruption caused the status to not converge normally”. Normal translation_failed is still prioritized for recover, and you should not route all failed articles through reconcile-attempts just because a single Ctrl+C occurred.


7. GLM 1301 Content Safety Filtering: Using ChatGPT Manual Fallback

If summary generation has been explicitly confirmed via execution evidence as a 1301 content safety filter, and repeating recover will only trigger the same deterministic rejection again, stop consuming GLM requests. The goal here is also not to modify the original article to “bypass” the model, but to preserve the legacy article as is, and have ChatGPT act as an external manual fallback to complete this article.

During manual fallback, you must first complete the Chinese summary, and then have ChatGPT generate the English title, English summary, and full English body based on the complete Chinese Gutenberg source. After manually saving to WordPress, do not directly modify the local JSON, and do not delete the original excerpt_generation_failed evidence; instead, first run a read-only Preview:

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

python3 bin/history-migration.py mark-manual-completed \
    --post-id 文章ID

mark-manual-completed will re-read the production environment, checking the Chinese and English post IDs, publish status, Polylang bidirectional relationship, as well as the English title and body. For excerpt_failed, it will additionally require the Chinese post_excerpt to be non-empty; if the Chinese summary has not yet been completed, it will explicitly show Chinese excerpt is empty and maintain 允许人工完成: 否.

Only after the Preview explicitly shows “manual completion allowed: yes” and “write operation: no” do you execute the confirmation:

Bash
python3 bin/history-migration.py mark-manual-completed \
    --post-id 文章ID \
    --confirmed

--confirmed will not modify WordPress again, nor will it rewrite the original automatic execution evidence as successful. It only records workflow_status=completed in the local coordination state, and adds manual_completion.status=confirmed, manual_completion.method=manual_external. This allows the batch to converge normally while fully preserving the initial GLM 1301, retry, and recovery history.

This is an anomaly fallback path, and does not replace the normal run-ready --execute. Normal articles still have their summaries and English override translations generated automatically by the repository; only when the model explicitly rejects the request and continuing automatic retries is meaningless do we switch to ChatGPT + manual save + mark-manual-completed.

14. Real Validation Results and Process Evolution

Batch 1: mixed-syntaxhighlighter-20260729-01

A total of 20 articles. Manual Gutenberg normalization, mark-converted, production read-only validation, and formal execution were all ultimately completed. During the process, GLM HTTP 400, English override translation HTTP 500, and Polylang SSH timeout occurred, all of which were resolved via limited retries or single-article recovery.

Batch 2: mixed-syntaxhighlighter-20260730-01

A total of 20 articles. The first production read-only validation passed 18 articles, with 2 failing due to batch read-only SSH query timed out, but both remained at awaiting_readonly_validation and all passed after limited retries. The formal execution ultimately resulted in 1 article with translation_failed, which was completed at the time using single-article resume.

Batch 3: mixed-syntaxhighlighter-20260731-01

A total of 20 articles. Before execution, we first encountered WP_REST_NONCE expiration causing multiple REST preflight checks to consecutively return 403, which recovered after resetting the Nonce. Subsequently, 8819 entered translation_failed due to an English title translation failure; after manually modifying the Chinese title and body, the old execution baseline became invalid, and a direct resume was safely intercepted.

At that time, restart-from-current was newly added and actually validated, completing the single-article execution after rebuilding the recovery generation with the current production version. This case proved that recovery must distinguish whether the Chinese source has changed, but the old process still required manually selecting multiple commands.

Subsequent Case: mixed-syntaxhighlighter-20260804-02

Of the 20 articles in this batch, 19 completed normally; 6965 stopped at translation_failed due to a Protected Token validation failure. This case further exposed the issues of overly coarse HTTP 500 classification, deterministic errors being retried invalidly, and overly scattered recovery commands.

After completing the error classification, SSH 255 limited retry, and unified recover refactoring, all 21 related targeted tests passed, and the full test suite increased from 402 to 404 items, all passing; in the real environment, 6965 recovery completed, and the preview and --execute safe no-op for completed were also verified. The final changes were pushed to the remote repository via commit 52d6fac.

August 14, 2026 Case: mixed-syntaxhighlighter-20260814-03

In this batch of 20 articles, 19 completed automatically, while Chinese article 7756 consecutively returned HTTP 400 during the Chinese summary generation phase. The initial three attempts within the batch all returned 400, the first recover --execute encountered an independent TimeoutError, and the subsequent recovery still returned 400. After further reading the latest execution evidence, it was confirmed that Zhipu’s actual response was error.code=1301, containing content safety filtering information.

After offline inspection using the production code extract_excerpt_source(), the original body of 7756 was 8228 characters, the cleaned content actually sent to the summary model was only 892 characters, and the full payload was about 3427 UTF-8 bytes, which did not trigger the 20k / 28k truncation. Therefore, there was no evidence supporting “input too long” here; the strongest evidence was model content safety filtering.

Ultimately, the legacy article was not modified to pass GLM. Instead, ChatGPT was used to manually generate the Chinese summary, English title, English summary, and full English body. After saving to WordPress, mark-manual-completed --post-id 7756 read-only acceptance was executed first, and then --confirmed was used to converge the status. Finally, 7756 was recorded as workflow_status=completed, manual_completion.method=manual_external, and the original excerpt_generation_failed evidence was retained; the batch recovered to completed=20, remaining=0.

These cases illustrate that whether the next batch can begin should not be based on the historically cumulative retry_exhausted, but on the current failed=0, blocked=0, remaining=0, “latest unfinished batch: none”, and “suggest creating next batch: True”.


15. The Fixed Daily Process Now Actually Required

After multiple batches of real runs and unified recovery refactoring, daily operations can be condensed into the following nine steps:

  1. Execute status and summary to confirm whether there is an unfinished batch;
  2. Only create a new fixed batch from the remaining empty-summary candidates when “suggest creating next batch: True”, with a maximum of 20 articles per batch;
  3. Set the day’s batch_id and csv_file;
  4. Output the backend edit URLs for the current batch all at once;
  5. Check the current structure article by article: articles that are already clean Gutenberg only need confirmation; normalize Classic / Mixed / unknown to Gutenberg; if SyntaxHighlighter or other legacy code structures still exist, migrate and verify them according to existing rules;
  6. Batch execute mark-converted;
  7. After checking the Cookie / REST Nonce, execute the production read-only validation; a non-empty variable does not mean the credentials are still valid. When encountering 403 rest_cookie_invalid_nonce, update the credentials first, and then perform limited retries for sporadic SSH failures still stuck at awaiting_readonly_validation;
  8. After confirming WP_ADMIN_COOKIE, WP_REST_NONCE, ZHIPU_API_KEY, execute run-ready --execute;
  9. Finally, use run-ready Preview, status, summary to simultaneously confirm completed, remaining, failure statuses, and integrity, before deciding whether to start the next batch.

If a stable single-article failure status has already formed after formal execution, you no longer need to manually determine resume or restart-from-current first. Uniformly execute recover --post-id 文章ID to view the strategy; only enter WordPress to manually modify the Chinese article when the program explicitly prompts that it is necessary, and after saving, execute the same command again and add --execute. If the evidence has clearly confirmed a deterministic model content safety filter like 1301, continuing automatic retries is meaningless; instead, follow the ChatGPT manual fallback process in Section 13 to complete the Chinese summary and English content, and then use mark-manual-completed Preview / --confirmed to converge to completed. If it is a local interruption like Ctrl+C leaving behind translation_started / execution_in_progress, it is a status coordination anomaly; first execute reconcile-attempts and sync-execution according to the specialized process in Section 13, and then return to resume Preview / Execute.

The production validation phase is still handled separately: awaiting_readonly_validation + SSH timeout → limited retry; validation_failed → fix and then validate-live --refresh. Failures in the formal execution phase are prioritized for unified handling by recover; only when the model explicitly rejects the request and normal automatic recovery is meaningless do we enter the ChatGPT manual fallback.


Summary

As of August 12, 2026, the preceding Gutenberg + Code Block Pro, Gutenberg + SyntaxHighlighter, Mixed + SyntaxHighlighter, and the last 5 anomalous articles have all been completed, and the legacy migration status reached 45 fixed batches with all 857 articles completed. Subsequent read-only production inventory confirmed that 424 more Chinese empty-summary legacy articles met the processing criteria, so the same SOP continues to expand to the remaining legacy articles.

The only real change in this phase is the candidate entry: articles are no longer required to belong to the Mixed category or contain SyntaxHighlighter, but instead all legacy articles that are “published in Chinese, have an empty summary, have a published English counterpart, and are not yet in a fixed batch” are selected. The fixed batches, manual confirmation, mark-converted, production read-only validation, summary generation, English override translation, recover, and completed state machine all continue to be used.

The Mixed Gutenberg + SyntaxHighlighter phase does not represent a failure of the previous migration plan, but rather an additional layer of Classic / Mixed legacy structure in the candidate articles themselves. The original fixed batch, manual conversion, production validation, summary generation, English override translation, and completed state machine can still continue to be used; it is only necessary to include full Gutenberg normalization in manual confirmation and production validation.

As real batches continued to progress, the SOP also added four key capabilities: first, distinguishing genuine transient network errors from deterministic Protected Token validation errors based on WordPress JSON responses, and further identifying Zhipu’s nested error.code=1301 content safety filtering; second, automatically determining whether the Chinese source has changed via the unified recover command, and selecting resume, restart_from_current, retry_excerpt_generation, blocked, or none; third, addressing translation_started / execution_in_progress left by local interruptions like Ctrl+C, using generation-scoped reconcile-attempts, counter_drift fixes, and sync-execution to make the status converge again; fourth, allowing ChatGPT to complete manual summaries and English translations when the model cannot continue due to deterministic content safety filtering, and then safely confirming external completion via mark-manual-completed while retaining the original failure evidence.

Now, the daily main flow is still “create fixed batch → manual conversion → production validation → batch execution → summary confirmation”. Normal single-article failures still prioritize unified recover preview and execution; only a few status coordination anomalies such as local interruptions, orphaned attempts, or inconsistencies between execution evidence and workflow require entering specialized recovery; when the model explicitly returns a deterministic rejection like 1301, you can switch to the ChatGPT manual fallback and wrap up with mark-manual-completed. Throughout this entire process, you should never force status advancement by deleting state, clearing evidence, or manually modifying JSON.

系列导航

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

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