Cloudflare リソース
noisy-crow が依存する Cloudflare リソースの一覧と用途。noisy-crow-web (UI) と noisy-crow-events (Slack 受信) の 2 つの Worker が同じ KV / D1 / R2 / Vectorize / AI バインディングを共有 します。バインディングは各 Worker の wrangler 設定 (apps/noisy-crow-app/wrangler.toml と apps/noisy-crow-events/wrangler.toml) に定義され、そこに書かれた id / database_id / index_name / bucket_name がそのまま正となります。両 Worker で同一リソースを指すよう揃えること。
リソース一覧
| リソース | バインディング名 | 用途 | 備考 |
|---|---|---|---|
| KV Namespace | SESSIONS_KV | Slack イベントの重複排除 (event:<eventId> → completed / completed_no_results 等) | TTL 1 時間 |
| D1 Database | EMOJI_DB (tied-workspace) | 絵文字メタデータの正本 (emojis) + 同期ジョブ状態/履歴 (sync_jobs / sync_job_data) | スキーマは packages/d1/ で統一管理 |
| R2 Bucket | EMOJI_BUCKET (noisy-crow-emoji) | 除外絵文字リスト JSON | 単一オブジェクトで十分 |
| Vectorize Index | EMOJI_VECTORIZE (noisy-crow-emoji) | 絵文字ベクトル | 1024 次元 / cosine |
| Workflow | SYNC_WORKFLOW (noisy-crow-sync) | 絵文字インデックス同期ジョブ | UI Worker のみ。class は SyncEmojiVectorsWorkflow |
| AI Gateway | AI_GATEWAY_ID (env) | Google AI Studio へのプロキシ | 直接 SDK は使わない |
KV / D1 / R2 / Vectorize / AI のバインディングは noisy-crow-web と noisy-crow-events の両 Worker が共有する (旧構成の Consumer Worker / Bot Container や Cloudflare Queue は廃止済み)。Queue は無い。Workflow SYNC_WORKFLOW だけは UI Worker (apps/noisy-crow-app/wrangler.toml) にのみ定義する (events Worker は同期に関与しない)。D1 (tied-workspace) のスキーマ (migrations) はどの Worker にも置かず リポジトリ共通の packages/d1/ で統一管理 する (events Worker は D1 を実行時に参照しないが、共有コアの composition root がバインディングを要求する)。UI Worker は cron trigger (毎日 18:00 UTC = 03:00 JST) も持ち、diff 同期を自動起動する。
それぞれの設計上の役割
SESSIONS_KV
Slack の Events API は 同一イベントを複数回配送 しうる。events Worker の /slack/events の処理 (processStampRequest) 開始時に event:<eventId> を確認し、既に記録があれば skipped_duplicate で抜けて冪等性を保つ。Queue を廃止したため自動リトライ / DLQ は無く、重複抑止はこの KV 1 箇所に集約されている。
キー設計:
event:<eventId> → "completed" | "completed_no_results" 等TTL は 3600 秒 (1h)。Slack の再送ウィンドウより十分長く、その後は再処理されてもよい (Slack 側で reaction が既にあれば already_reacted で no-op)。
EMOJI_DB (D1 tied-workspace)
絵文字メタデータと同期ジョブ状態の 正本 (ベクトルだけは Vectorize)。Vectorize 側の metadata は二重管理になっても気にしない方針。
スキーマはアプリ配下ではなく リポジトリ共通の packages/d1/ で統一管理する。適用される migrations は migrations/tied-workspace/、drizzle 定義は schema/tied-workspace.ts (migration と対で管理する型付きの正本。ただし noisy-crow-core のインフラ層は現状 drizzle ではなく素の D1 prepared statement で読み書きするため runtime では未使用) (定義の正本・適用手順・規約は packages/d1/README.md)。分離が必要になる場合はテーブル名のプレフィックスではなくデータベースを分ける方針 (database 粒度)。
テーブル:
emojis (name PK, url, description, last_updated, embedding_model, caption_model)
sync_jobs (instance_id PK, mode, phase, total, processed, deleted, errors(JSON), failure_reason, started_at, updated_at)
sync_job_data (instance_id PK, plan(JSON), captions(JSON), created_at)emojis: 絵文字メタデータの正本。get/listStored(sync の差分計算用の軽量一覧) /listPage/listWithVectorsが読み、upsertMany/deleteManyが Vectorize と同時に書く。/emojisの一覧はlistPage()がORDER BY name LIMIT ? OFFSET ?+LIKE(name / description 部分一致、%_\はエスケープ) で 1 ページ分だけ読むため、表示コストが総件数に比例しない。embedding_model/caption_modelに生成時のモデル名を記録し、rebuild の差分計算に使う。モデル記録の無いエントリは rebuild 時に「現行モデルではない」とみなされ再処理される。- 手動編集された caption は
caption_model = "manual"(MANUAL_CAPTION_MODEL) で記録する。rebuild で caption は再生成されず、embedding が古い場合はベクトルだけ作り直される (caption はSyncPlan.pinnedCaptionsで維持)。
sync_jobs: 同期ジョブ状態の正本 + 履歴。putStatusが instance_id で upsert し、getStatusは最新行 (= 現在/直近のジョブ)、/syncsページの履歴はlistJobs()(startedAt 降順 + ページング)。sync_job_data: ジョブの plan / captions (Workflow step 間の受け渡し用の大きな JSON)。完了後は不要で、D1 に TTL が無いためputPlanのたびに 7 日より古い行を削除する。
D1 の 1 クエリあたりの bound parameter は 100 個までなので、IN (...) の削除は 100 件、batch の upsert は 50 statement ずつに分割している。
旧 EMOJI_META_KV (廃止方針・未参照)
かつて絵文字メタデータ (emoji-meta:<name>) と同期ジョブ状態 (sync:*) の正本だった KV namespace (id 3cd1830546d745baaa7de8e59a89b4bc)。利用は全て D1 (EMOJI_DB) に移行済みで、どの Worker もバインドしておらずコードから参照されない。旧データの KV → D1 移行は行わない — D1 が空でも sync (再バッチ。Rebuild または diff) を 1 回実行すれば Slack から全件再構築できる。namespace 自体は Cloudflare 上に残してあり、再バッチ後の動作を確認したら wrangler kv namespace delete で削除してよい (手動編集 caption は KV にしか無いため、引き継ぎたい場合は削除前に控えて /emojis/:name から再設定する)。
EMOJI_BUCKET (R2)
除外絵文字の JSON を 1 オブジェクトに保存する想定。アクセス頻度が低く、件数も多くないため Vectorize 側の metadata フィルタは使わずファイルベースで運用する。
EMOJI_VECTORIZE
Gemini embedding (gemini-embedding-001, outputDimensionality: 1024 + L2 正規化) の 1024 次元ベクトルを cosine で保存。searchSimilar() は topK で取得し、similarity >= threshold だけ返す。Vectorize の getByIds は最大 20 件 (超過時 VECTOR_GET_ERROR 40007) のためバッチして取得する (listWithVectors)。旧 Voyage multimodal-3 も同じ 1024 次元だったため index は作り直していない (ベクトル空間は別物なので Rebuild で全件再生成 が必要)。
AI Gateway
https://gateway.ai.cloudflare.com/v1/<accountId>/<gatewayId>/google-ai-studio/... のプロキシ。AI 呼び出しは全て Google AI Studio プロバイダーに集約している。
Voyage は使わない: 旧実装は
voyage/multimodalembeddingsで multimodal embedding を生成していたが、Voyage は AI Gateway のサポートプロバイダーに含まれず、全リクエストがAiGatewayError2008 Invalid provider で弾かれる。embedding も Gemini に移行した (2026-06)。
- Gemini embedding:
gemini-embedding-001(envGEMINI_EMBEDDING_MODELで上書き可) で caption テキストを embedding する。outputDimensionality: 1024で Vectorize index の次元に合わせ、3072 以外は正規化されずに返るため自前で L2 正規化する。同期ジョブではbatchEmbedContentsに 50 件まとめて 投げる (generateDocumentEmbeddings。API 上限は 100 requests/リクエスト)。検索クエリはembedContent+taskType: RETRIEVAL_QUERY(generateQueryEmbedding、単発)。画像は embedding に直接渡さず、caption (画像→テキスト) を経由して反映する。 - Gemini (生成):
gemini-3.5-flash(envGEMINI_MODELで上書き可。旧gemini-2.0-flashは 2026-06 に廃止され全リクエストが error code 5 で失敗するようになった) で- 絵文字画像のキャプション生成 — Batch API (
models/<model>:batchGenerateContentに一括投入 →v1beta/batches/<id>をポーリング)。非同期だが 50% 割引で、全件再構築のような大規模ジョブに向く (submitCaptionBatch/checkCaptionBatch) - スレッド文脈から reaction 用フレーズ生成 (
generateReactionText、同期) - 可視化クラスタのカテゴリ名 + 説明生成 (
generateClusterLabels、同期generateContent+responseSchemaの JSON 出力)。全クラスタを 1 リクエストにまとめ、クラスタごとに API を呼ばない
- 絵文字画像のキャプション生成 — Batch API (
secret GOOGLE_API_KEY がある Worker は 全リクエストに x-goog-api-key を付与 して Gateway 経由で Google に直接認証する (Gateway はプロバイダーキーを素通しするため、リクエストは引き続き AI Gateway 経由)。キーが無い Worker のみ AI Gateway の Unified Billing (Cloudflare 経由でプロバイダーのモデルを利用し、利用料も Cloudflare 経由で課金される方式) にフォールバックする (旧 VOYAGE_API_KEY は削除してよい)。
このゲートウェイは Authenticated Gateway (ゲートウェイ自体の認証) が有効 (Unified Billing の前提条件でもある)。全リクエストへ cf-aig-authorization: Bearer <AI_GATEWAY_TOKEN> ヘッダーが必要で (無いと Google に転送される前に AiGatewayError 2009 Unauthorized で弾かれる)、プロバイダーキーを送らないため このトークンが AI 呼び出しの唯一の認証情報。トークンはダッシュボード (AI Gateway → Settings → Authentication) で発行し、両 Worker の secret AI_GATEWAY_TOKEN (必須) に投入する。CfAiGateway が全呼び出しに自動付与する。
Unified Billing は信頼できる認証経路ではない: 認証注入はサポート対象のモデル × エンドポイントにだけ行われ、対象外は認証なしで素通しされて Google 側で 403 PERMISSION_DENIED (unregistered callers) になる。本リポジトリが使う全ての組み合わせ — Batch 系 (
batchGenerateContent/batches/*/batchEmbedContents)、gemini-3.5-flash のgenerateContent、gemini-embedding-001のembedContent(検索クエリ) — で注入漏れを 2026-06 に本番確認済み (streaming や未対応モデルでも同種の注入漏れがコミュニティ報告されている)。このためCfAiGatewayはGOOGLE_API_KEYがあれば全リクエストにx-goog-api-keyを付与し (この分の利用料は Google 直課金)、Batch 系はキー未設定だと明示エラーにする。キー未設定時は Unified Billing にフォールバックする実装だが、実際に動く組み合わせは確認されておらず、両 Worker でGOOGLE_API_KEYを必須として運用する。
SYNC_WORKFLOW (Workflow noisy-crow-sync)
絵文字インデックス同期ジョブの実行基盤。HTTP リクエストの実行時間制限を受けず、step ごとの自動リトライ・状態永続化・step.sleep による長時間ポーリング (Gemini Batch の完了待ち) ができる。インスタンス ID は startEmojiSync() が発行する UUID で、D1 sync_jobs の instance_id と一致する。同時実行は 1 ジョブ (起動時に実行中チェック)。
不変条件
- 上記バインディング名 (
SESSIONS_KV等) は共有コア (packages/noisy-crow-core) の infrastructure 層と、両 Worker の wrangler 設定 (apps/noisy-crow-app/wrangler.tomlとapps/noisy-crow-events/wrangler.toml) に出現する。変更時は必ず 3 箇所をまとめて更新し、両 Worker が同一リソースを指すよう揃える (SYNC_WORKFLOWは UI Worker のみの例外)。 - embedding モデルを 次元数が変わる モデルへ更新する場合、Vectorize index は次元固定のため作り直しが必要: 新しい index を作成 → 両 Worker の wrangler 設定の
index_nameを更新 → デプロイ → Web から Rebuild を実行、の順で行う (旧 index はリビルド完了を確認してから削除)。次元が同じなら Rebuild だけでよい。 - 新しい Cloudflare リソースを足すときは、
ports.tsの抽象を先に決めてから infra 実装を追加する (テスト容易性のため)。 - 本番リソース ID は両 Worker の wrangler 設定に直書きしている。Secrets は Worker ごとに
wrangler secret put -c apps/<worker>/wrangler.tomlで設定し、リポジトリには絶対に入れないこと。