マキモノ
業務自動化無料✅ 公式検証済みv1.0.0 / 更新

文字化けで宛先が決まらない1件がキューを永久に詰まらせるのを直す

自動処理キューで宛先解決に失敗したアイテムは「失敗」ではなく「スキップ」になり、exit 0 のまま毎晩リトライされ続ける。詰まりの検出、化けた文字列から元テキストを同定する指紋照合、壊れにくい第2キーへの許可リスト型フォールバック、そして「対象外にする」出口までを実例で通す。

出品者: kim@orgiast.jp📖 読込 約3,860トークン (約6円)💰 コスパ 9
トークン節約メーター80%節約
ゼロからAIに作らせた場合4.5万トークン
このMDを読ませた場合9,000トークン

3.6万トークンの節約 (API料金換算で約54円分)。 要件定義・技術調査・試行錯誤ぶんのトークンがまるごと不要になります。※ 出品者申告とレビューに基づく推定値。モデル・タスク内容により変動します。

この巻物について

文字化けで宛先が決まらない1件がキューを永久に詰まらせるのを直す」は、業務自動化カテゴリのAI指示書(MDファイル)です。自動処理キューで宛先解決に失敗したアイテムは「失敗」ではなく「スキップ」になり、exit 0 のまま毎晩リトライされ続ける。詰まりの検出、化けた文字列から元テキストを同定する指紋照合、壊れにくい第2キーへの許可リスト型フォールバック、そして「対象外にする」出口までを実例で通す。この巻物をAIに読み込ませると、ゼロから設計・調査する場合に比べて 約3.6万トークン(API料金換算で約54円)・80%のトークンを節約できます。

カテゴリ
業務自動化
対応AI
claude-code、cursor、codex-cli
ライセンス
商用利用可 (再販不可)
価格
無料
ゼロから開発時
約4.5万トークン
この巻物使用時
約9,000トークン
節約量
約3.6万トークン (約54円)
更新日
2026-08-30

使い方 (AIに渡す3つの方法)

いちばん簡単なのはワンライナー。Claude Code のターミナルに貼るだけです。

⬇ .md をダウンロード
claude "https://makimono-md.vercel.app/api/v1/files/md-024db359/raw を読み込んで、この指示書どおりに実装して"
claude-codecursorcodex-cliライセンス: 商用利用可 (再販不可)

中身

文字化けで宛先が決まらない1件がキューを永久に詰まらせるのを直す

自動化パイプラインの「キューから1件取り出して、宛先を決めて、投げる」型の処理は、 宛先が決まらないアイテムを『失敗』ではなく『スキップ』として扱うことが多い。 スキップは exit 0 で終わるので、そのアイテムは毎晩静かにリトライされ続け、誰も気付かない

この指示書は、実際に2日間詰まっていた1件を題材に ①詰まりの検出 ②化けた文字列から正体を特定する方法 ③再発しない宛先解決の設計 ④「対象外にする」出口 の4つをまとめる。フィードバック中継に限らず、メール振り分け・Webhook ルーティング・チケット自動起票・ バッチのシャーディングなど「キー→宛先」を引くすべての処理に当てはまる。


0. 前提と用語

  • キュー: 未処理アイテムを保持する外部サービス(DB / Pub-Sub / 独自の中継 API など)。 取り出し(pending 取得)と完了通知(ack)が分かれているものを想定する。
  • 宛先解決: アイテムの属性(アプリ名・テナント名・製品名など)から、投げ先 (リポジトリ・チャンネル・担当キュー)を決める処理。
  • スキップ: 宛先が決まらないので何もしないこと。ack しないため、次回も同じアイテムが返ってくる。

1. 詰まりを検出する

症状はログにしか出ない。 「毎晩正常終了しているのに、キューの残件数が減らない」が唯一の兆候。

<tool>: 未マッピングなのでスキップ app=<化けた文字列> message_id=<id>
作成: 0件 / スキップ: 1件(未マッピング:1)/ 残り: 1件

チェックすべきこと:

  1. 残り が何日も同じ値で止まっていないか。0 に落ちない残件は詰まりの疑い。
  2. 同じ message_id が複数日のログに出ていないか。出ていればそのアイテムは永久リトライ状態。
  3. スキップ理由の内訳(未マッピング:N)が毎回同じか。

--dry / --dry-run 相当の「投げずに判定だけする」フラグが無ければ、まずそれを足す。 本番キューに実データを当てて安全に観測できないと、この後の切り分けが全部できない。


2. アイテムの実体を「見る」— 端末の表示を信用しない

ログに出た文字列が化けていても、それが本当に壊れているのか、端末のコードページで化けて見えているだけかは 表示からは判別できない。必ずコードポイント列を出す。

// pending を1件取り出して、問題のフィールドのコードポイントを出す
const name = String(item.app_name ?? '');
console.log([...name].map((c) => c.codePointAt(0).toString(16)).join(' '));

判定:

  • U+FFFD(置換文字)が混ざっている → データそのものが壊れている(不可逆。表示の問題ではない)
  • 正常な日本語が並ぶ → 端末の表示だけの問題。データは無事なので、原因は別のところにある

さらに、同じアイテムの他のフィールドが健全かどうかを必ず見る。 今回の題材では app_name が壊れている一方、source_url は完全に健全だった。 壊れやすいフィールドと壊れにくいフィールドは共存するので、ここが後の設計の分かれ目になる。


3. 化けた文字列から元テキストを同定する(指紋照合)

U+FFFD が混ざった時点で復元は不可能だが、元が何だったかを同定することはできる。 「レガシーな1バイト/2バイト系エンコーディングのバイト列を、UTF-8 として decode した」場合、 壊れ方には決まった規則があり、それが指紋になる。

規則(レガシー→UTF-8 誤 decode):

元のバイトUTF-8 として読むと
0x000x7Fそのまま ASCII 文字として生き残る
単独の 0x800xBF、不正なリードバイトU+FFFD に潰れる(ここで情報が消える)
偶然そろった正しい UTF-8 の多バイト列実在の文字として残る(例: C7 97U+01D7

つまり 「生き残った ASCII 文字」と「U+FFFD の個数と位置」の並びが、元の文字列の指紋になる。

同定手順:

  1. 元テキストの候補を立てる(サービス名・定型文・想定される文面)。
  2. 候補を疑わしいレガシーエンコーディングでバイト列にする。
  3. そのバイト列を UTF-8 として replacement 付きで decode する。
  4. 得られた並びが、実データのコードポイント列と一致するか照合する。
// 候補を検証する(<legacy> は疑うエンコーディング名に置き換える)
const bytes = Buffer.from('<候補テキスト>', '<legacy>');
const decoded = new TextDecoder('utf-8').decode(bytes); // 不正バイトは U+FFFD になる
console.log([...decoded].map((c) => c.codePointAt(0).toString(16)).join(' '));
// これが実データのコードポイント列と一致すれば、候補が元テキストで確定

なぜこれをやる価値があるのか: 化けたアイテムを「読めないゴミ」として機械的に捨てると、 本当は対応が必要な報告だった場合に失われる。逆に、同定した結果それが 「テスト投稿」「対応不要」だと分かれば、自信を持って対象外にできる。 今回の題材では本文末尾が定型の「対応不要」文だと確定し、不要なチケットを起票せずに済んだ。

文字列がどのタイミングで壊れたかも押さえておく。多くはシェル経由で非 ASCII を argv 渡ししたときに起きる (OS やシェルがネイティブ実行ファイルへの引数をレガシーコードページへ変換するため)。 検証スクリプトに非 ASCII を渡すときは argv を通さず、ヒアドキュメント/ファイル/stdin で渡す。


4. 再発しない宛先解決の設計

原因は「文字化け」そのものではなく、宛先解決を単一キーの完全一致に賭けていたこと。 そのキーが壊れた瞬間、アイテムは永久に宙に浮く。

4-1. 壊れにくい第2キーへフォールバックする

人が入力した名前より、システムが生成した値(URL・ホスト名・ID)のほうが化けにくい。

export function resolveTargetForItem(item, mapValue = '', hostMapValue = '') {
  // 第1キー(従来どおり)。ここで解決できた場合の挙動は絶対に変えない。
  return resolveByName(item?.app_name, mapValue)
    // 第2キー。第1キーで解決できなかったときだけ使う。
    || resolveByUrl(item?.source_url, mapValue, hostMapValue);
}

既存の優先順位を変えないことが重要。第1キーで解決できるアイテムの宛先が変わると、 直したつもりが別の事故になる。回帰テストで固定する(後述)。

4-2. フォールバックは必ず許可リスト方式にする

URL から未知の宛先を推測してはいけない。無関係な宛先へ投げる事故になる。 既にマッピング表にある値にしか解決しないよう縛る。

export function resolveByUrl(sourceUrl, mapValue = '', hostMapValue = '') {
  let hostname;
  try {
    hostname = new URL(String(sourceUrl ?? '').trim()).hostname.toLowerCase();
  } catch {
    return null; // 空文字・不正 URL でも例外を投げない
  }
  if (!hostname) return null;

  // 明示表が最優先(独自ドメインなど、自動導出が効かないケース用)
  const explicit = parseHostMap(hostMapValue)[hostname];
  if (explicit) return explicit;

  // 自動導出: ホストの先頭ラベルが、既存の表の値と一致するときだけ解決する
  const firstLabel = hostname.split('.')[0];
  const map = { ...DEFAULT_MAP, ...parseMap(mapValue) };
  return Object.values(map).find((target) => nameOf(target).toLowerCase() === firstLabel) || null;
}

設計上の勘所:

  • hostname を使うhost ではない)。ポート付き URL でも動く。
  • 小文字化して比較する。ホスト名は大文字小文字を区別しない。
  • 不正 URL で例外を投げない。ここで throw するとキュー全体が落ちる。
  • 設定値の検証を第1キーと同じ厳しさで掛ける。宛先文字列をシェル経由のコマンド (チケット CLI など)に渡すなら、コマンドとして解釈される文字を通さない:
    if (/^[A-Za-z0-9.-]+$/.test(host) && /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(target)) { /* 採用 */ }
    
  • 明示表を環境変数で受ける<PREFIX>_HOST_MAP="host=owner/name,...")。 自動導出が効かない独自ドメインを、コード変更なしで救えるようにする。

4-3. どの経路で解決したかを必ずログに出す

if (!primaryTarget) {
  console.log(`<tool>: 第1キーで解決できないため URL のホストで解決 host=${hostname} target=${target}`);
}

無言でフォールバックすると、後から「なぜこの宛先になったのか」を追えない。 誤配送が起きたときに、原因がマッピング表なのかフォールバックなのか切り分けられなくなる。


5. 「対象外にする」出口を用意する

宛先解決を強化しても、どうしても投げるべきでないアイテムは残る(テスト投稿、重複、対応不要と明記された報告)。

出口が「投げる」しかないと、次の二択になってしまう:

  • 読めないタイトルのチケットを本番に残す
  • 永久に詰まらせる

どちらも悪い。ack だけして対象外にする経路を用意する。

// 完了通知(ack)だけを送って、キューから外す
const res = await fetch(ackUrl, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', authorization: `Bearer ${secret}` },
  body: JSON.stringify({ message_id: TARGET_ID }),
});

運用上の条件:

  • 対象外にする根拠を残す(手順3の同定結果、元メッセージへのリンク)。ack は取り消せない。
  • 元アイテムがチャット等に残っているなら、そのリンクをアイテムが持っているか確認してから外す。 一次ソースが消えないことが、ack を安全にする唯一の担保。
  • 繰り返し発生するなら、ツール本体に --dismiss <id> のようなサポートされた出口として実装する。

6. 検証(ここを飛ばすと直っていない)

6-1. 単体テストは「実測値」で書く

化けた文字列は適当な代用文字列で代替しない。実データのコードポイント列をそのまま再現する。 代用すると、実際の壊れ方(U+FFFD の位置と個数、偶然生き残った実在文字)を再現できず、 テストが緑でも実データが通らない

const brokenName = String.fromCodePoint(0xfffd, 0x77, 0xfffd, /* …実測列… */);

固定すべきケース:

#ケース期待
1設定表の解析(正常値・不正な宛先・不正なホスト・空要素)不正値だけ落ちる
2URL の先頭ラベルから既知の宛先を自動導出設定なしで解決
3表に無いホストnull(推測しない)
4明示表が自動導出より優先明示表の値
5空文字・不正 URL例外を投げず null
6化けた名前 + 健全な URL候補になる
7正常な名前(回帰防止)従来と同じ宛先
8化けた名前 + URL も無い候補にならない
9正常な名前 + 別宛先を指す明示表(優先順位の回帰防止)名前側が勝つ

6-2. 実データで通し確認する

テストが緑でも、実データの1件が通る証拠にはならない。 必ず本番キューに --dry を当てて、詰まっていたそのアイテムが解決されることを目視する。

<tool>: 第1キーで解決できないため URL のホストで解決 host=… target=…
<tool>: 作成予定 target=… title=…

6-3. 修正が「実際に走る場所」に届いているか確認する

main にマージしただけでは、定期実行しているマシンでは動かない。 定期タスクが参照している作業ツリーが、古いブランチや未同期のままになっていないか必ず確認する:

# 定期実行が参照しているディレクトリで
git branch --show-current
grep -c '<新しく足した関数名>' <対象ファイル>   # 0 なら修正は届いていない

0 だった場合は「直った」と報告してはいけない。同期の段取りまでが作業の範囲。


7. チェックリスト

  • キューの 残り が何日も減っていないログを確認した
  • 問題フィールドをコードポイント列で見て、データ破損か表示問題かを判別した
  • 同じアイテムの他のフィールドが健全かを確認した
  • 化けた文字列を指紋照合で同定し、対応要否を判断できる根拠を得た
  • 第2キーへのフォールバックを許可リスト方式で実装した
  • 第1キーで解決できる場合の挙動が変わっていない(回帰テストで固定)
  • どの経路で解決したかをログに出している
  • 「対象外にする」出口があり、一次ソースが消えないことを確認した
  • 実測コードポイントを使ったテストが緑
  • 本番キューに --dry を当てて、詰まっていた1件が解決されることを確認した
  • 定期実行しているマシンの作業ツリーに修正が届いていることを確認した

よくある質問

「文字化けで宛先が決まらない1件がキューを永久に詰まらせるのを直す」とは何ですか?

自動処理キューで宛先解決に失敗したアイテムは「失敗」ではなく「スキップ」になり、exit 0 のまま毎晩リトライされ続ける。詰まりの検出、化けた文字列から元テキストを同定する指紋照合、壊れにくい第2キーへの許可リスト型フォールバック、そして「対象外にする」出口までを実例で通す。

どれくらいトークン(費用)を節約できますか?

ゼロから開発すると約4.5万トークンかかりますが、この巻物を使えば約9,000トークンで済みます。差し引き約3.6万トークン(API料金換算で約54円)・80%の節約です。

どうやって使いますか?

無料です。MDファイルを Claude Code などのAIに読み込ませるだけ。ワンライナーをターミナルに貼れば実装が始まります。要件定義や技術調査を省いて実装だけにトークンを使えます。

どのAIツールに対応していますか?

claude-code、cursor、codex-cli に対応しています。

商用利用できますか?

ライセンスは「商用利用可 (再販不可)」です。

🤝 自分でAIを動かすのは、まだ不安…という方へ

この巻物の内容を、AIを使うプロに丸ごと任せることもできます。姉妹サービスAI代行堂なら「LINEで頼むだけで、仕事が完成」。

AI代行堂を見る →

関連する巻物

この巻物、誰かのトークンも救えます

𝕏 で節約レシートをシェア