文字化けで宛先が決まらない1件がキューを永久に詰まらせるのを直す
自動処理キューで宛先解決に失敗したアイテムは「失敗」ではなく「スキップ」になり、exit 0 のまま毎晩リトライされ続ける。詰まりの検出、化けた文字列から元テキストを同定する指紋照合、壊れにくい第2キーへの許可リスト型フォールバック、そして「対象外にする」出口までを実例で通す。
約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 のターミナルに貼るだけです。
claude "https://makimono-md.vercel.app/api/v1/files/md-024db359/raw を読み込んで、この指示書どおりに実装して"
中身
文字化けで宛先が決まらない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件
チェックすべきこと:
残りが何日も同じ値で止まっていないか。0 に落ちない残件は詰まりの疑い。- 同じ
message_idが複数日のログに出ていないか。出ていればそのアイテムは永久リトライ状態。 - スキップ理由の内訳(
未マッピング: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 として読むと |
|---|---|
0x00–0x7F | そのまま ASCII 文字として生き残る |
単独の 0x80–0xBF、不正なリードバイト | U+FFFD に潰れる(ここで情報が消える) |
| 偶然そろった正しい UTF-8 の多バイト列 | 実在の文字として残る(例: C7 97 → U+01D7) |
つまり 「生き残った ASCII 文字」と「U+FFFD の個数と位置」の並びが、元の文字列の指紋になる。
同定手順:
- 元テキストの候補を立てる(サービス名・定型文・想定される文面)。
- 候補を疑わしいレガシーエンコーディングでバイト列にする。
- そのバイト列を UTF-8 として replacement 付きで decode する。
- 得られた並びが、実データのコードポイント列と一致するか照合する。
// 候補を検証する(<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 | 設定表の解析(正常値・不正な宛先・不正なホスト・空要素) | 不正値だけ落ちる |
| 2 | URL の先頭ラベルから既知の宛先を自動導出 | 設定なしで解決 |
| 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で頼むだけで、仕事が完成」。
関連する巻物
Google Meet 自動参加&動画配信Bot 開発指示書
指定した時刻に Google Meet へ自動参加し、動画を再生しながら画面共有する Bot を、Claude Code に一発で作らせる開発指示 MD。朝会の定例動画配信・ウェビナーの自動放送に。
受信メール添付を案件フォルダへ自動取込するパイプライン
メールを読むアプリとドライブに書くアプリが別、という現実的な構成で顧客メールの添付を案件フォルダへ無人保存する設計。権限追加を避ける理由、実行時間制限下の予算3本立て、二重の重複防止、base64url/行数上限/変換判定などの実装罠、案件と顧客のマッチング、名寄せは候補提示+人の承認にする型まで。
Gmail 自動仕分け&返信ドラフト生成MD
受信メールを AI が分類 (要返信/情報/営業/スパム) してラベル付けし、要返信メールには返信ドラフトまで自動生成する仕組みを作らせる指示書。DWD (ドメイン全体委任) 設定手順込み。
この巻物、誰かのトークンも救えます
𝕏 で節約レシートをシェア