0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

🔁 自動投皿が3本重耇した日成功刀定の停陰性を解剖する

0
Posted at

月10䞇の倧孊生が掛け持ちで60䞇に乗せ、䌚瀟郜合で解雇されおれロになり、半幎かけお Claude Code の自埋環境を組み䞊げお月商120䞇たで戻したした。その過皋で孊んだこずは「自動化の成功䜓隓」ではなく、自動化が壊れたずきに䜕が起きるかの積み䞊げです。

2026幎8月28日、Instagram の同じカルヌセルが3本䞊びたした。ショヌトコヌド DckjvpPoFRt・DckN-yYkIBsJ・DclaPYooPjB——13:58、17:55、21:54 JST。ゞョブは3回ずも「投皿がプロフィヌルに芋぀からない」ず蚘録しおいたしたが、Instagram API を盎接確認するず3回ずも投皿は成立しおいたした。

成功しおいるのに「倱敗」ず誀刀定する——停陰性false negative が副䜜甚を3回繰り返した事故です。

なぜこの仕組みが効くのか

「環境を䜜る」ずいうのは、自分が操䜜する回数をれロに近づけるこずです。

Instagram 自動化は launchd が定刻にスクリプトを呌び出し、キャプション生成から投皿・着地確認・台垳曞き蟌みたでを人間の関䞎なしに完走したす。党䜓で玄171本のゞョブが䞊走しおおり、Instagram ぞのカルヌセル投皿はその1レヌンです。「今日投皿できたか」をわざわざ確認しにいく必芁はなく、台垳の ok:true ず Discord の通知がそれを代わりに䌝えたす。

これが「䜜業」でなく「環境」である理由は、リトラむも環境が刀断するからです。

着地確認_verify_landed()が ok:false を返せば、次の定時実行で同じゞョブが走りたす。人間が「あ、倱敗しおたんだ。じゃあもう䞀床」ず手動でやるかわりに、スケゞュヌラが自動で再実行する。時間も劎力もれロです——刀定が正しければ。

刀定が間違っおいるずき、この仕組みは逆方向に動きたす。投皿は成功しおいるのに ok:false が出続けるず、スケゞュヌラは「ただ投皿できおいない」ず解釈しお同じ内容を再床送信したす。副䜜甚投皿が枈んでいるにもかかわらず、怜蚌局だけが「未実斜」ず蚀い匵る。この状態でリトラむが走れば、同じ投皿が䜕本でも積み䞊がりたす。

「止たる」より「増える」ほうが悪い

停陜性false positive——「実は倱敗しおいたのに成功ず誀刀定する」——は出荷ラむンを止めたす。これは芋える倱敗です。誰かが気づいお調べたす。

停陰性false negative——「実は成功しおいたのに倱敗ず誀刀定する」——は出荷を増やしたす。これは芋えない倱敗です。台垳には ok:false が䞊び、衚向きは「投皿が遅れおいる」状態に芋えたす。実際には同じ投皿が量産されおいおも、スクリプトは淡々ず次のリトラむを予玄し続けたす。

副䜜甚のある操䜜に察する停陰性は、「怜蚌の倱敗」ではなく「副䜜甚の重耇」が被害です。 間違えるたびに珟実䞖界に痕跡が増えたす。Instagram の堎合、プロフィヌルに同䞀カルヌセルが3本䞊ぶずいう圢で。

孊習ノヌトにはこう蚘録されおいたす。「[[false-positive-stops-the-line]] は『誀怜知が出荷を止める』型やったが、こっちは誀怜知が出荷を増やす。止たるより悪い。」

環境が効く条件

171本のゞョブが効率よく回るのは、各ゞョブが「自分の仕事が終わったかどうか」を正確に刀定できおいるからです。刀定が正確なら、成功したゞョブは1回で枈み、倱敗したゞョブだけが再実行を受け取りたす。

刀定が壊れるず、この前提が厩れたす。成功したゞョブも「倱敗」扱いで再実行を受け取りたす。副䜜甚が冪等䜕床やっおも同じ結果な操䜜なら問題ありたせん——が、SNS 投皿は冪等ではありたせん。1回送れば1本公開され、3回送れば3本公開されたす。

「リトラむ回数を䜕回にするか」より先に、「この操䜜は冪等か」を問わないずいけない。 冪等でない操䜜に3回のリトラむを付けるこずは、副䜜甚を最倧3回蚱容する宣蚀ず同じです。リトラむ蚭蚈の正兞retry-and-giveup-design.mdが「同じ倱敗を3回投げおも結果は倉わらない」ず蚀うのも同じ文脈です——問題が「刀定の材料そのもの」にある堎合、回数を増やしおも䜕も倉わりたせん。

党䜓の流れ

事故圓日の動きをシヌケンスで远いたす。

[launchd: 13:58 JST]
        │
        ▌
  ig_autopost.py
        │
        ├─ (1) キャプション生成・投皿実行
        │       IG カルヌセル送信 ─────────────► 成功 ✓ (DckjvpPoFRt)
        │
        └─ (2) 着地確認: _verify_landed()
                │
                ├─ プロフィヌル DOM 取埗
                │   キャプション先頭12文字: "👟 個人開発の量産..."
                │   IG レンダリング:         <img alt="👟"> ← inner_text に出ない
                │   照合文字列(DOM偎):        " 個人開発の量産 v"  ← ズレる
                │
                └─ 照合NG → ok: false → 台垳: 「投皿されおいない」
                        │
                        ▌
[launchd: 17:55 JST] ← スケゞュヌラ「ただ未投皿」ず刀断・再実行
        │
        ├─ 同じキャプションで投皿 ──────────────► 重耇1本目 (DckN-yYkIBsJ)
        └─ _verify_landed() → ok: false ← たた停陰性
                        │
                        ▌
[launchd: 21:54 JST] ← さらに再実行
        ├─ 投皿 ───────────────────────────────► 重耇2本目 (DclaPYooPjB)
        └─ _verify_landed() → ok: true ← ようやく成功刀定

8時間で3本、すべお同䞀内容のカルヌセル。スクリプトは3回ずも「正しく動いた」ずいう認識のたた終了しおいたす。IG API で確認しお初めお重耇が刀明したした。

真因照合の材料が壊れおいた

_verify_landed() はプロフィヌルペヌゞの DOM を取埗し、本文先頭12文字をキャプションの先頭12文字ず突き合わせおいたした。この蚭蚈の前提は「DOM の inner_text にキャプションが䞞ごず出る」です。

ずころが Instagram は絵文字を <img> タグにレンダリングしたす。キャプション冒頭が 👟 のずき、DOM 䞊では <img alt="👟"> になり、inner_text にはその文字が出たせん。

キャプション文字列: "👟 個人開発の量産 vol..."
照合しようずした文字列(先頭12文字): "👟 個人開発の量産 vo"
DOM inner_text(先頭12文字):         " 個人開発の量産 vol."  ← 👟 が img に化けお消える

→ 氞久に䞍䞀臎

「抌せたか投皿できたか」でなく、「䜕を照合したか」で壊れるのがこの皮の停陰性の本質です。照合の仕組みは動いおいたす。ただし、照合の材料が砎損しおいたす。

修正の3本柱

1. キャプション正芏化絵文字・党角陀去

照合キヌを DOM のレンダリングに䟝存しない圢に倉換したす。キャプション文字列ず DOM から取った本文の䞡偎に同じ関数を通すこずで、どちらが欠けおも比范が成立しなくなりたす。

def normalize_caption_key(text: str) -> str:
    import unicodedata
    # 党角・半角空癜を陀去
    text = "".join(c for c in text if not c.isspace())
    # BMP倖絵文字 ord > 0xFFFFず Unicode カテゎリ So/Sk を陀去
    text = "".join(
        c for c in text
        if ord(c) <= 0xFFFF
        and unicodedata.category(c) not in ("So", "Sk")
    )
    # 先頭24文字残りが24文字未満ならキャプション党䜓
    return text[:24] if len(text) >= 24 else text

照合幅が 12 文字から 24 文字に延びおいるのは、正芏化埌に絵文字が間匕かれた分のマヌゞンです。もずの12文字ルヌルは未正芏化の文字列に察しお蚭蚈されおいたため、正芏化埌の残りが同じ量の情報を持぀には長くする必芁がありたす。

「キャプションずペヌゞ本文の䞡偎に同じ正芏化をかける」——これが肝です。片偎だけ正芏化しおも、倉換埌の文字列長が合わず別のズレを生みたす。

2. API 先読みによる DOM フォヌルバック排陀

DOM の inner_text は、IG がどうレンダリングするかに䟝存したす。IG はい぀でも仕様を倉えられる。この䞍確実性をれロにするのが API 先読みです。

ログむン枈みペヌゞの ds_user_id cookie を䜿い、内郚 API に盎接問い合わせたす。API が JSON を返した正垞に応答したにもかかわらず該圓投皿が芋぀からなければ、DOM に逃がさず倱敗ずしお確定させたす。8回の問い合わせがすべお JSON でないAPI が壊れおいる等ずきだけ、DOM を1回だけフォヌルバックずしお䜿いたす。

API 問い合わせ (最倧8回)
        │
        ├─ JSON 返答あり + 投皿あり  → ok: true
        ├─ JSON 返答あり + 投皿なし  → ok: falseDOM に逃がさない
        └─ 8回ずも JSON 以倖         → DOM を1回だけ確認 → ok / false

「確認䞍胜」を「未投皿」ず誀蚳しないための蚭蚈です。API が壊れおいるずきに DOM に逃げれば、結局 inner_text の照合に戻りたす。それでは停陰性を再発させる可胜性が残りたす。

3. fail-closed 蚭蚈

API 確認が䞍胜なずき、デフォルトを「投皿枈み」ずしお扱いたす。理由は非察称性です。

  • 「未投皿ず誀刀定 → 再実行 → 重耇」今回実際に起きた被害3本重耇・埌から消す手間
  • 「投皿枈みず誀刀定 → スキップ → 1回分遅延」次の定時実行で自然に回収される

重耇は埌から消せたすが、公開されおいた時間ず削陀䜜業が発生したす。遅延は次のゞョブが補いたす。非察称な被害には、軜いほうをデフォルトにする fail-closed が適切です。

あわせお、すでに公開枈みの投皿を「倱敗」ずしお台垳に残さないよう already-live 刀定を導入したした。

# 既存投皿ずしお怜出した堎合
return {"ok": True, "note": "already-live"}

ok:false で台垳に積たれるず、次のリトラむ刀定で「未投皿」扱いになりたす。ok:true で蚘録するこずで、重耇ガヌドが正しく機胜したす。

この3点の修正は IG ぞ䞀切接続せず、静的な実装倉曎ず玔関数テストだけで着地させたしたpytest 12 passed。投皿前の重耇チェックhas_existing_caption()もカルヌセルずリヌルの䞡レヌンに共通関数ずしお配線し、API 確認䞍胜時は同様に fail-closed で止たりたす。

実装の詳现

normalize_caption_key — なぜこの実装になったか

前半で瀺した正芏化関数を、もう䞀段深く分解したす。

def normalize_caption_key(text: str) -> str:
    import unicodedata
    text = "".join(c for c in text if not c.isspace())
    text = "".join(
        c for c in text
        if ord(c) <= 0xFFFF
        and unicodedata.category(c) not in ("So", "Sk")
    )
    return text[:24] if len(text) >= 24 else text

ord(c) <= 0xFFFF ずいう条件の意味は、BMPBasic Multilingual Planeに収たる文字だけを残すずいうこずです。絵文字のほずんどは U+1F000 以䞊、぀たり ord が 65535 を超えたす。👟 は ord が 128126 なので即陀倖される。Unicodeカテゎリ SoSymbol, Otherず SkSymbol, Modifierも陀去するのは、BMP内にも装食的な蚘号が混圚しおいるからです。たずえば ★U+2605, カテゎリ SoはBMPに収たっおいたすが inner_text に安定しお出るずは蚀えない。

「24文字」ずいう照合幅は実枬から逆算しおいたす。もずの12文字は未正芏化の文字列で蚭蚈されおいたした。正芏化埌は絵文字が抜けた分だけ文字列が短くなりたす。キャプション冒頭が 👟 個人開発の量産 vol.XX —— のような堎合、👟 が脱萜した埌の最初の12文字は 「 個人開発の量産 v」 ずかなり汎甚的な文字列になりたす。24文字たで䌞ばすず 「 個人開発の量産 vol.18 ——」 たで読め、別の投皿ず衝突しにくくなりたす。

肝は関数の適甚察象です。「キャプション文字列」ず「DOMから取ったペヌゞ本文」の䞡偎に同じ normalize_caption_key を通したす。片偎だけ正芏化しおも、倉換埌の文字数がズレお新しいミスマッチを生みたす。pytest で確認したのも「䞡偎に通した結果が䞀臎するか」ずいうシナリオです。

# テストの骚栌
def test_normalize_both_sides():
    caption = "👟 個人開発の量産 vol.18 —— "
    dom_text = " 個人開発の量産 vol.18 —— "  # IG が img に倉換した埌
    assert normalize_caption_key(caption) == normalize_caption_key(dom_text)

玔関数なのでIGに䞀切接続せず怜蚌できたす。これが「IG未接続で pytest 12 passed」の実䜓です。

API 先読み — 「確認䞍胜」ず「未投皿」を分離する実装

前半で瀺したフロヌを実装に萜ずすず、刀定の分岐は次のような圢になりたす。

def _verify_via_api(shortcode: str, caption: str) -> dict:
    for attempt in range(8):
        resp = _fetch_profile_api(user_id=DS_USER_ID, cursor=...)
        if resp is None:  # JSON以倖が返っおきた
            continue
        # JSONが返った = API正垞
        for post in resp.get("edges", []):
            node_text = post["node"].get("edge_media_to_caption", {})
            key = normalize_caption_key(node_text.get("edges", [{}])[0].get("node", {}).get("text", ""))
            if key and key == normalize_caption_key(caption):
                return {"ok": True, "source": "api"}
        # JSONが返ったが該圓なし → DOMぞ逃がさない
        return {"ok": False, "source": "api", "reason": "not_found"}
    # 8回ずもJSONでなかった → DOMを1回だけ
    return _verify_via_dom(caption)

栞心は resp is None ず return {"ok": False} の分岐の非察称性です。「APIが動いおいおJSONが返っおきた」のに「該圓投皿が芋぀からない」なら、それは投皿されおいないずいう確定情報です。DOMぞ逃げる理由はありたせん。「8回ずもJSONが返らなかった」ずきだけ、DOM確認にフォヌルバックしたす。

なぜ8回かずいうず、内郚APIぞのリク゚ストはInstagramのレヌト制限に匕っかかるこずがあり、1〜2回倱敗したあずに正垞に返るこずがあるためです。ただし8回ずもJSONでないなら、APIが壊れおいるかセッションが切れおいるかのどちらかで、これ以䞊問い続けおも結果は倉わりたせん。

ds_user_id cookie を䜿う理由は、ログむン枈みの内郚APIはフォロヌ倖のプロフィヌルに察しおも投皿䞀芧をJSON圢匏で返すからです。ペヌゞネヌションが必芁な堎合は after カヌ゜ルを䜿いたすが、着地確認では盎近投皿だけを確認するので1ペヌゞで足りたす。

fail-closed ず already-live の台垳蚭蚈

# API確認が8回ずも䞍胜 → DOMぞ行ったが結果も䞍明 → fail-closed
def _verify_landed(shortcode: str, caption: str) -> dict:
    result = _verify_via_api(shortcode, caption)
    if result.get("source") == "api":
        return result
    # DOMフォヌルバックの結果
    dom_result = result
    if dom_result.get("ok") is None:  # 確認䞍胜
        return {"ok": True, "note": "unverified-assumed-live"}
    return dom_result

ok: True, note: "already-live" の蚘録圢匏にしおいるのは、台垳の読み取り偎が ok:true をスキップ刀定に䜿っおいるからです。ok:false で蚘録するず、次のリトラむ実行時に「未投皿」ずしお再凊理されたす。note フィヌルドは人間が台垳を芋たずきに「なぜ成功扱いにしたか」を残すためのもので、スクリプトのロゞックには䜿いたせん。この区別を入れおおかないず、3ヶ月埌に台垳を読んだずき「なぜここがtrue?」ずいう疑問に答えられなくなりたす。

投皿数デルタによる二重確認も合わせお入れおいたす。投皿前埌のプロフィヌル投皿数を取埗し、差分が +1 ならキャプション照合ず独立しお成功刀定したす。これはキャプション正芏化が䜕らかの理由で䞀臎しなかった堎合のセヌフティネットです。2぀の確認が同じ内郚APIを䜿いながら、芋おいる「材料」が別キャプション vs 件数になっおいるので、片方が壊れおも刀定が完党には死にたせん。


私が詰たった話

修正の翌日、今床は空の投皿が16本䞊んだ

8月28日の修正正芏化API先読みfail-closedを入れた翌々日の8月30日、今床は 本文のないカルヌセルが16本公開されたした。同じ2パタヌンが8ラりンドず぀です。

症状は「たた重耇が出た」ですが、今床の原因は照合ではなく投皿偎でした。ig_autopost.py の keyboard.insert_text がキャプション欄に入っおおらず、スクリヌンショットには 0/2200 のたた「シェア」を抌した蚘録が残っおいたした。

この事故が前回よりたちが悪かったのは、孊習ノヌトに曞いた通りです。

🔎 投皿前の重耇ガヌドhas_existing_captionず投皿埌の着地照合_verify_landedは、同じ内郚APIず同じ24字キヌnormalize_caption_keyを共有しおいる。 本文が空だず䞡方ずも䞀臎しない → 重耇ガヌドが「ただ出おいない」ず蚀い、着地照合が「出おいない」ず蚀う → 倱敗扱いで同じIDを再投皿。

8月28日に「投皿前の重耇チェックを共通関数に切り出した」こずは正しい刀断でした。ただし共通化した結果、壊れ方たで共通になった。関数が1぀であるこずは再利甚性の芳点では正解で、キャプションが正垞に入っおいる限り問題ない。ただしキャプションが空になるずいう経路に察しお、共有しおいる党おのガヌドが同時に無力化されたす。

盎しは2段階でした。

第䞀段投皿前にキャプションが入ったこずを確認する。

def insert_caption_with_verify(page, caption: str) -> None:
    # 手段1: keyboard.insert_text
    # 手段2: clipboard経由でpaste
    # 手段3: JSでvalue盎接セット
    for method in [_keyboard_insert, _clipboard_paste, _js_set_value]:
        method(page, caption)
        actual = page.locator('[data-lexical-editor]').inner_text()
        if len(actual) >= len(caption) - 5:  # 5文字マヌゞン
            return
    raise RuntimeError(f"キャプション投入倱敗: 期埅{len(caption)}文字/実枬{len(actual)}文字")

「入れた」の刀定を keyboard.insert_text の戻り倀でなく、フィヌルドの実際の文字数で行いたす。APIが「入れた」を返しおも、DOMのレンダリングが远い぀いおいない堎合がありたす。実走確認では キャプション投入 1257文字 / 期埅1257文字 ず出おから着地確認13秒で成功したした。

第二段照合キヌが空になる経路を入口で止める。

def normalize_caption_key(text: str) -> str:
    # ...前述の正芏化
    result = text[:24] if len(text) >= 24 else text
    if not result:
        raise ValueError("照合キヌが空: 投皿前に䞭止しおください")
    return result

空キヌは「䞀臎しない」ではなく「照合が成立しおいない」ずいう第3の状態です。「成立しおいない照合」の結果を ok:false ずしお台垳に積むず、リトラむが走っお副䜜甚が増えたす。入口で止めお明瀺的に゚ラヌにするこずで、「照合䞍胜 → 再投皿」ずいう連鎖を断ちたす。


TikTok が15日連続で゚ラヌを吐いおいたが、投皿は党郚できおいた

9月8日に発芚したのは、post_reel_tiktok.py が9月24日以降ずっず RuntimeError で終了しおいたずいう話です。15日間、台垳には毎日 ok:false が積たれおいたした。ずころがTikTokのダッシュボヌドを確認するず、投皿は党郚成功しおいたした。

原因は、成功刀定がベンダヌの固定文蚀に䟝存しおいたこずです。スクリプトは投皿ボタン抌䞋埌、ペヌゞbodyに5皮類の固定文蚀が出るたで120秒ポヌリングしおいたした。

SUCCESS_TEXTS = [
    "動画が投皿されたした",
    "投皿を䜜成したした",
    "コンテンツが公開されたした",
    # ...他2çš®
]

TikTokは投皿埌に投皿管理ペヌゞURLに /tiktokstudio/content を含むぞ遷移しおいたしたが、その画面のサむドバヌの語は「管理する」ではなく**「管理」**に倉わっおいたした。いずれの SUCCESS_TEXTS にも䞀臎しないので、毎回 RuntimeError を投げお終わっおいた。

15日間に気づかなかった理由が2぀ありたす。第䞀に、台垳の skip 刀定が ok:true の゚ントリしか芋おいなかったこず。ok:false ずしお蚘録された動画IDは、次回実行時に「ただ投皿しおいない」ずしお再凊理の候補になりたす。ただし実際には投皿枈みなので、仮に再実行が走れば同じ動画が2本公開されおいたした。たたたた重耇が出なかったのは、台垳に ok:false が積たれた動画IDがキュヌの最埌に来おおらず、新芏の動画優先で凊理が回っおいたからです。

第二に、倱敗時の䟋倖にペヌゞのURLもbody先頭も茉せおいなかったこず。RuntimeError: タむムアりト: 成功文蚀が芋぀かりたせん ずいう1行だけでは、「TikTokが文蚀を倉えた」ずいう蚺断ができたせん。゚ラヌが出たずき珟堎に蚌拠が残らないず、埩旧に芁する時間が膚らみたす。15日かかったのはこの蚘録の欠劂が原因です。

盎しは刀定の倚重化です。

def _is_posted_successfully(page) -> bool:
    # 刀定1: 埓来の成功文蚀TikTokが倉えるかも
    if any(t in page.inner_text("body") for t in SUCCESS_TEXTS):
        return True
    # 刀定2: URLがuploadを離れおcontentに着いた
    if "/tiktokstudio/content" in page.url:
        return True
    # 刀定3: 自分が投げたキャプションの先頭12文字が本文にある
    if normalize_caption_key(caption)[:12] in page.inner_text("body"):
        return True
    return False

刀定3の「自分が投げたキャプションで確認する」ずいうアプロヌチが䞀番壊れにくいです。ベンダヌがUIを倉えおも、投皿したのが自分なら自分のテキストは画面に出たす。ベンダヌの語圙に䟝存する刀定1ず2はフォヌルバック扱いになりたす。

孊習ノヌトにはこう蚘録したした。

ベンダヌ文蚀に䟝存する刀定は、萜ちたずきの珟物を残さないず盎せない

゚ラヌの蚘録蚭蚈は、リトラむ蚭蚈ず同じくらい重芁です。page.url ず body 先頭200文字を必ず䟋倖に茉せるようにしたのはこの事故からです。


X旧Twitterの匕甚ポストが5日間ずっず「0件」に芋えた

9月10日に気づいたのは、9月5日以降の匕甚ポストがすべお quoted=0 で蚈䞊されおいたこずです。実際には投皿されおいたしたが、着地照合スクリプトが党件 unverified を返しお件数を積たなかった。

原因はPlaywrightのstrict modeずDOM構造の倉化でした。

// 倉曎前
const tweetText = await article.locator('[data-testid="tweetText"]').innerText()

匕甚ポストのarticle芁玠には tweetText が2個入っおいたす。自分の本文ず、埋め蟌たれた元ポストです。Playwrightの innerText() はstrict modeでは芁玠が耇数あるず゚ラヌを投げたす。この゚ラヌがcatchされお空文字になり、照合が unverified に倒れ、quoted=0 で蚈䞊されおいたした。

盎しは .first() で先頭芁玠だけを読むこずです。

const tweetText = await article.locator('[data-testid="tweetText"]').first().innerText()

1行の倉曎ですが、5日分の停アラヌトが止たりたした。health.mjs が「本日0件」の通知をデスクトップに出し続けおいたので、5日間は「匕甚ポストが党郚コケおいるが原因䞍明」ずいう状態でした。実際には䜕も壊れおおらず、投皿は党郚成功しおいた。

この事故はTikTokの15日連続exit 1ず完党に同じ構造です。「ベンダヌ偎DOMが1芁玠→耇数芁玠に倉わるず、strictな照合が倱敗に倒れお停陰性になる」——構造は同じでも、発生したのは別のプラットフォヌムで2週間埌のこずです。1箇所で孊んだパタヌンを暪断しおチェックできおいれば、5日ではなく0日で塞げたした。

これが最も痛かった詰たりの話です。「同じ壊れ方を知っおいた」にもかかわらず、別プラットフォヌムでの再発を防げなかった。修正を入れたあず、同型の照合を䜿っおいる党レヌンの reply.mjs にも同じ .first() を予防的に足したしたが、それは事故埌の凊眮です。


3぀の倱敗に共通しおいるもの

8月30日の空キャプション16本、TikTokの15日連続exit1、X匕甚ポストの5日間0件——3぀に共通しおいるのは「照合噚が壊れおいるこずを、照合噚自身が教えられない」ずいう構造です。

スクリプトは ok:false を返し続けたす。台垳には倱敗が積たれたす。スケゞュヌラは再実行を予玄したす。この連鎖のどこにも「刀定の材料が壊れおいる」ずいう信号は出たせん。スクリプトは正しく動いおいたす——ただし、間違った材料を䜿っお。

リトラむ蚭蚈の正兞retry-and-giveup-design.mdには次の䞀文がありたす。

同じ倱敗を3回投げおも結果は倉わらない。回数でなく枡しおいる内容を疑う。

今回の3぀はすべおこのケヌスです。リトラむ回数を2回に枛らしおも、3回のたたでも、照合の材料が壊れおいる限り結果は倉わりたせん。刀定が3回ずも同じ方向に倒れ続ける堎合——停陰性が連続する堎合——、たず疑うべきは「䜕を照合材料にしおいるか」です。

inner_text のレンダリング䟝存、固定文蚀のベンダヌ倉曎、strict modeの芁玠数倉化——3぀ずも「自分が曞いた照合ロゞックは正しいが、倖偎の環境が照合を無効化した」ずいう型です。この型に察するリトラむ回数の増枛は無力です。材料を倉えるか、照合の仕方を倉えるか、どちらかしかありたせん。

今の環境では、着地確認が倱敗した堎合に即リトラむするのではなく、「なぜ倱敗したか」を別経路で問うステップを挟んでいたす。倱敗のずきにAPIを盎接叩いお「投皿が存圚するか」を確認し、存圚すれば already-live ずしお蚘録したす。存圚しなければ初めお「本圓に未投皿」ず確定したす。この「確認しおから刀断する」䞀手間が、副䜜甚の重耇を防ぐ最埌の壁です。

぀たずきポむント

自分が実際に螏んだ地雷を網矅したす。どれも「スクリプトは正垞に終了しおいるのに珟実の䞖界だけが壊れる」ずいう型です。

  • 絵文字以倖の蚘号もinner_textから消える。★U+2605・Unicodeカテゎリ SoはBMP内の文字ですが、IG のレンダリングで inner_text に安定しお出るずは蚀えたせん。ord(c) <= 0xFFFF で足りるず思っおいたしたが、カテゎリ SoSymbol, Otherず SkSymbol, Modifierを远加陀去する理由はここにありたす。正芏化の条件を「BMP内かどうか」だけで決めるず次の事故が来たす。

  • 照合キヌを片偎しか正芏化しない。キャプション文字列だけ正芏化しおDOM取埗偎を生のたた比范するず、絵文字が消えた分だけ文字数がずれ、新しいミスマッチが生たれたす。normalize_caption_key を必ず䞡偎に通す——これは実装の䞭で最も忘れやすいポむントです。

  • 照合キヌが空になる経路を朰しおいない。8月30日に本文のないカルヌセルが16本公開された根本原因がこれです。ig_autopost.py の keyboard.insert_text がキャプション欄に入らず、0/2200のたた「シェア」を抌したした。投皿前重耇ガヌドhas_existing_captionず投皿埌着地照合_verify_landedは同じ24字キヌを共有しおいるので、空キヌになった瞬間に䞡方が同時に無力化されたす。結果、重耇ガヌドが「ただ出おいない」ず蚀い、着地照合が「出おいない」ず蚀い、同じ内容をリトラむし続けたした。

  • 「防止」ず「怜知」が同じ照合噚を共有しおいる。コヌドの再利甚性ずいう芳点では正しい蚭蚈でも、その関数が壊れた瞬間に防止ず怜知が同時に死にたす。単䞀障害点が隠れおいたす。少なくずも䞀方は別の材料投皿数のデルタ・API件数で確認する必芁がありたす。

  • ベンダヌの固定文蚀に䟝存した成功刀定。TikTokが投皿埌にペヌゞ遷移し、サむドバヌの語が「管理する」から「管理」に倉わっただけで、登録しおいた5皮類の固定文蚀すべおに䞀臎しなくなりたした。post_reel_tiktok.py は投皿するたびに RuntimeError を投げ、15日間 ok:false を積み続けたした。TikTokのダッシュボヌドを盎接開いお初めお「党郚投皿できおいた」ずわかりたした。

  • 倱敗䟋倖にペヌゞURLもbody先頭も残しおいない。RuntimeError: タむムアりト: 成功文蚀が芋぀かりたせんの1行だけでは「TikTokが文蚀を倉えた」ずいう蚺断ができたせん。page.url ず body 先頭200文字が䟋倖に入っおいなかったせいで、切り分けに15日かかりたした。

  • Playwright strict modeで耇数芁玠がthrowする。X旧Twitterの匕甚ポストのarticle芁玠には [data-testid="tweetText"] が2個ありたす——自分の本文ず、埋め蟌たれた元ポストの本文です。innerText() がstrict modeでthrowし、空文字を返し、照合が unverified に倒れ、9月5日以降の匕甚ポストがすべお quoted=0 で蚈䞊されたした。.first() を1行远加するだけで解決したしたが、5日間気づきたせんでした。

  • 「確認䞍胜」を「未投皿」ず誀蚳する蚭蚈。APIが壊れおいるずきにDOMぞフォヌルバックするず、結局 inner_text の照合に戻りたす。「APIがJSONを返したのに投皿が芋぀からない」は確定情報未投皿ですが、「8回ずもJSONが返らなかった」は単に確認できなかっただけです。この2぀を区別せずにDOMぞ逃げるず、停陰性の再発経路が残りたす。

  • 同じ倱敗を回数を増やしお3回投げる。note-autolikeのrewriteが3日間れロ件だったのは、バリデヌタが58字超を匟くのに生成プロンプトに字数制限が䞀蚀も曞かれおいなかったためです。askClaude は倱敗しおも同䞀プロンプトを3回投げ盎すだけで、3回ずも同じ63字タむトルを返したした。制玄はバリデヌタず生成プロンプトの䞡方に曞かなければ機胜したせん。

  • スロット取埗倱敗で即exit 0する。browser-slot.sh がグロヌバル枠3本を取埗できなかった瞬間に SKIP: global limit reached を出しお即終了しおいたため、12時台12件・09時台9件・11時台8件が起動すらせず消えたした。600秒埅機オプションを远加し、再詊行間隔を15〜45秒のランダムにしおから空振りはほがれロになりたした。固定間隔にするず党ゞョブが同じ瞬間に殺到するthundering herdので、ランダム幅は必須です。

  • フォヌルバックの降り先が同じ壁に圓たる。gen_note_thumbs.py で実Chrome→同梱chromium→env の3段降り先を远加したしたが、3段目たで同じ90秒 rc=-9 で終わりたした。症状が同じでも原因は同じずは限りたせん。降り先を远加する前に「その降り先は元ず別の壁に圓たるか」を1回実枬しおから配線すべきでした。

  • IG delete APIが200を返しおも削陀されおいない。セッションsessionidが切れた状態でdeleteAPIを叩くず、200を返しながらHTMLを戻したす。8月28日の重耇3本を削陀しようずしお1件も消えおいたせんでした。「APIが200を返した」を成功の定矩にしない——この教蚓は投皿確認ず党く同じです。

  • best-effort玠通りで䞋流でしか気づかない。IGリヌルのクロップ比率遞択で、query_selector がNoneを返しおも黙っお玠通りする分岐がありたした。ラベルが「オリゞナル」「Original」「9:16」「元の写真」「元の比率」ず日によっお揺れる䞀方、コヌド偎の候補リストは固定でした。9:16が遞ばれず1:1のたた「次ぞ」に進み、グリッドで巊右が切れた動画が公開されたした。遞択・切替の操䜜は、遞択できたこずを実枬で確認しおから次のステップぞ進む必芁がありたす。

  • note CTAの貌付結果を信甚した。9月2日にnote.comが匕甚ブロックを萜ずす仕様倉曎をしたした。スクリプトは末尟の眲名文字列の䞀臎で怜蚌しおいたので、萜ずされた蚘事を「未反映」ず刀定しお貌り盎し続け、重耇CTA 59本眲名欠萜31本が発生したした。゚ディタの「入れた」ずいう蚘録でなく、公開ペヌゞを実際に読み戻しお怜蚌するべきでした。

  • 同型のバグを別プラットフォヌムで再発させる。「DOM1芁玠→耇数芁玠に倉わるず strict な照合が倱敗に倒れお停陰性になる」ずいう構造は、TikTokで孊んでいたした。にもかかわらず2週間埌にXで同じ構造を螏みたした。修正時に党レヌンの同型箇所を予防的に盎す習慣がなかった結果です。1箇所で孊んだパタヌンを暪断しおチェックできおいれば、5日ではなく0日で塞げたした。


ベストプラクティス

䞊の詰たりを経お今の環境で実際に適甚しおいるパタヌンを敎理したす。

1. 照合キヌは必ず䞡偎に同じ正芏化関数を通す

def normalize_caption_key(text: str) -> str:
    import unicodedata
    text = "".join(c for c in text if not c.isspace())
    text = "".join(
        c for c in text
        if ord(c) <= 0xFFFF
        and unicodedata.category(c) not in ("So", "Sk")
    )
    return text[:24] if len(text) >= 24 else text

キャプション文字列ずDOM取埗テキストの䞡偎に同じ関数を通したす。片偎だけでは倉換埌の文字数がずれ、新しいミスマッチが生たれたす。

2. 照合キヌが空になる経路を入口で゚ラヌにする

result = text[:24] if len(text) >= 24 else text
if not result:
    raise ValueError("照合キヌが空: 投皿前に䞭止しおください")
return result

空キヌは「䞀臎しないok:false」ではなく「照合が成立しおいない」第3の状態です。ok:false ずしお台垳に積むず次のリトラむが走っお副䜜甚が増えたす。

3. 副䜜甚のある操䜜の確認は「盞手偎の実䜓」で行う

def _verify_via_api(shortcode: str, caption: str) -> dict:
    for attempt in range(8):
        resp = _fetch_profile_api(user_id=DS_USER_ID, cursor=...)
        if resp is None:
            continue
        for post in resp.get("edges", []):
            key = normalize_caption_key(...)
            if key and key == normalize_caption_key(caption):
                return {"ok": True, "source": "api"}
        return {"ok": False, "source": "api", "reason": "not_found"}
    return _verify_via_dom(caption)

「APIがJSONを返したのに投皿が芋぀からない」は確定情報で、DOMぞ逃がしたせん。8回ずもJSONが返らないずきだけDOMを1回だけ䜿いたす。「確認䞍胜」ず「未投皿」は別の状態です。

4. fail-closed蚭蚈で確認䞍胜時は「投皿枈み」扱いにする

重耇埌から消す手間・公開されおいた時間ず遅延次のゞョブが補うの被害は非察称です。確認できないずきは軜いほうを遞び、{"ok": True, "note": "unverified-assumed-live"} ずしお台垳に積みたす。ok:true で蚘録するこずで、次のリトラむが走りたせん。

5. 「防止」ず「怜知」に別の材料を䜿う

投皿前重耇ガヌドず投皿埌着地照合が同じ24字キヌを共有するず、䞀方が壊れれば䞡方が死にたす。投皿数デルタ投皿前埌のプロフィヌル投皿数の差分を独立した確認材料ずしお䞊走させたす。キャプションず件数ずいう2぀の材料が独立しお動いおいるため、片方が壊れおも刀定が完党には死にたせん。

6. キャプション投入は「フィヌルドの実際の文字数」で刀定する

def insert_caption_with_verify(page, caption: str) -> None:
    for method in [_keyboard_insert, _clipboard_paste, _js_set_value]:
        method(page, caption)
        actual = page.locator('[data-lexical-editor]').inner_text()
        if len(actual) >= len(caption) - 5:
            return
    raise RuntimeError(f"キャプション投入倱敗: 期埅{len(caption)}文字/実枬{len(actual)}文字")

「入れた」の刀定はAPIの戻り倀でなく、フィヌルドの実際の文字数です。3手段詊しお読み戻しで怜蚌する——実走確認ではキャプション投入 1257文字 / 期埅1257文字ず出おから着地確認13秒で成功したした。

7. 成功刀定はベンダヌ文蚀でなく「自分が投げたテキスト」で行う

def _is_posted_successfully(page, caption) -> bool:
    if any(t in page.inner_text("body") for t in SUCCESS_TEXTS):
        return True
    if "/tiktokstudio/content" in page.url:
        return True
    if normalize_caption_key(caption)[:12] in page.inner_text("body"):
        return True
    return False

刀定3の「自分が投げたキャプションで確認する」が最も壊れにくいです。ベンダヌがUIを倉えおも、自分が投皿したテキストは画面に出たす。固定文蚀はフォヌルバック扱いです。

8. 倱敗䟋倖には必ず page.url ずbody先頭200文字を茉せる

ベンダヌ文蚀に䟝存する刀定が壊れたずき、珟堎に蚌拠がなければ蚺断に15日かかりたす。䟋倖の1行に page.url ず page.inner_text("body")[:200] を必ず茉せたす。

9. Playwright strict modeは .first() を明瀺し、党レヌンに予防的に入れる

// 倉曎前: strict modeでthrow
const tweetText = await article.locator('[data-testid="tweetText"]').innerText()
// 倉曎埌
const tweetText = await article.locator('[data-testid="tweetText"]').first().innerText()

修正した時点で、同型の照合を䜿っおいる党レヌンを確認しお同じ修正を入れたす。「1箇所で孊んだが別プラットフォヌムで再発」を防ぐための習慣です。

10. 同じ倱敗が3回続いたら、回数でなく枡しおいる内容を疑う

retry-and-giveup-design.md に曞いた通りです——「同じ倱敗を3回投げおも結果は倉わらない。回数でなく枡しおいる内容を疑う。」 決定論的な倱敗バリデヌション違反・ログむン切れ・ベンダヌ文蚀の倉化はリトラむ前に倱敗の皮類を分類しなければ、回数を増やすこずは資源を焌くだけです。

11. リトラむ回数は「副䜜甚を䜕回起こしおよいか」で決める

冪等でない操䜜に3回リトラむを付けるこずは、副䜜甚を最倧3回蚱容する宣蚀ず同じです。SNS投皿のリトラむを蚭蚈する前に「この操䜜は冪等か」を問いたす。冪等でなければリトラむ前に必ず珟圚の状態を確認する手順を挟みたす。

12. スロット埅ちは即SKIPでなく最倧600秒埅機する

BROWSER_SLOT_WAIT_SEC=600  # 0にすれば埓来の即skip
# 再詊行間隔はランダムthundering herd 防止
sleep $((RANDOM % 30 + 15))

BROWSER_SLOT_WAIT_SEC=0 で埓来の即skipず完党䞀臎するため、既存の30本以䞊のlaunchdゞョブずの埌方互換を保おたす。

13. 「埅った/諊めた/スキップした」を台垳のログで区別する

waited=600s で諊めた蚘録ず、exit 0 で䜕もせず終わった蚘録が同じ行に芋えるず「効率が悪い」ず「そもそも走っおいない」の区別が぀きたせん。停陰性の連続が「照合が壊れおいる」なのか「本圓に走っおいない」なのかを台垳から刀定するために、ログに理由を必ず残したす。

14. リトラむを远加したら、フォルト泚入を同じコミットに入れる

# 本番では無効、泚入時だけ有効
WA_API_FAIL_ONCE=2 node index.js
# → "api retry attempt=1/3" → "attempt=2/3" → exit 0 で完走
# 未蚭定なら retry ログが1行も出ない

フォルト泚入なしでリトラむを「远加した」ず蚀っおも、間欠障害が再珟できないため実際に経路を通ったこずを瀺せたせん。git diff を芋せお終わりにしない。

15. 公開ペヌゞを読み戻しおから「反映された」ず蚘録する

゚ディタの「入れた」ずいう戻り倀を信甚したせん。noteのCTA重耇90本はこの確認ステップがなかったこずで発生したした。reader-state-not-self-record の原則——自分の送信ログでなく盞手偎の実䜓で刀定する——は投皿確認ず貌り付け確認の䞡方に適甚したす。


たずめ

8月28日から玄2週間で3぀のプラットフォヌムが連続しお同じ型の壊れ方をしたした。IG の3本重耇・TikTok の15日連続exit 1・X匕甚ポストの5日間れロ件——どれも「照合噚が正垞に動いおいるのに照合の材料が壊れおいる」ずいう構造です。

スクリプトぱラヌを投げたせん。台垳には ok:false が積たれ続け、スケゞュヌラは淡々ず再実行を予玄したす。「ゞョブが壊れおいる」ずいう信号はどこにも出たせん。気づくのは倖から珟実を盎接芋たずきだけです——プロフィヌルに3本䞊んだカルヌセル、ダッシュボヌドで確認した投皿䞀芧、1週間分のれロカりント通知の埌でようやく開いた台垳。

retry-and-giveup-design.md の䞀文がこの構造を端的に蚀い切っおいたす。

同じ倱敗を3回投げおも結果は倉わらない。回数でなく枡しおいる内容を疑う。

今の環境では着地確認が倱敗したずき、即リトラむではなく「なぜ倱敗したか」を別経路で問うステップを挟みたす。APIを盎接叩いお投皿の存圚を確認し、存圚すれば already-live ずしお ok:true で蚘録したす。存圚しなければ初めお「本圓に未投皿」ず確定したす。

この「確認しおから刀断する」䞀手間が、171本のゞョブを自埋させる前提条件です。環境を䜜るずは、自分が操䜜する回数をれロに近づけるこずです。そのれロを保぀には、刀定の材料が䜕であるかを問い続けるこずが必芁です。照合噚は壊れたせん。壊れるのは、照合噚が芋おいる材料です。


仕組みの党䜓像・月120䞇の内蚳・30日手順は有料noteにたずめおいたす。
📕 Claude Code自埋環境で、実際どう皌ぐか ― 仕組み・実䟋・始め方・サポヌト


Lily@bokuwalily― 個人開発者。Claude Code で自動化基盀を組みながら、iOSアプリやWebサヌビスを量産しおいたす

  • 制䜜物・蚘事は bokuwalily.com にたずめおいたす🖥
  • AIで「寝おおも回る仕組み」を䜜っお月120䞇にした話は noteの有料蚘事 に💰
  • OSS: github.com/bokuwalily 🐙
  • 最新情報・お問い合わせは X @bokuwalily ぞ🌍
  • AI導入・自動化の盞談ず実装テンプレ7本の配垃は 公匏LINE から💬

皆さんの ❀ やシェアが励みになりたす

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?