Skip to content

ダミ声カラス (noisy-crow) 仕様書

apps/noisy-crow-app (UI Worker) / apps/noisy-crow-events (Events Worker) / packages/noisy-crow-core (共有コア) (公開名: ダミ声カラス) の正本となる仕様書です。今後このアプリに変更を加える際は、先にこのドキュメントを読み、必要に応じて更新を加えてから実装に入ってください

アプリ全体像

noisy-crow は Slack に投稿されたメッセージの文脈を解析し、関連する絵文字リアクションを自動で付与する Slack bot です。旧 slack-stamp-bot をクリーンアーキテクチャ + Cloudflare スタックで再構成したもので、現在は共有コア packages/noisy-crow-core + 2 つの Cloudflare Worker (noisy-crow-web / noisy-crow-events) で構成します。

名称種別役割
packages/noisy-crow-core共有コア (framework 非依存 TS パッケージ)クリーンアーキテクチャ層 (domain / application / infrastructure / composition) を所有し exports で公開。両 Worker が依存する。React は含まない
noisy-crow-web (apps/noisy-crow-app)React Router v7 (framework mode / Cloudflare Workers)管理 UI (loader / action) と 絵文字インデックス同期の Workflow noisy-crow-sync (+ 毎日の cron 起動)。共有コアのユースケースを app/lib/deps.server.ts 経由で呼ぶ。Slack イベント処理は持たないbetter-auth (共有認証基盤 packages/auth) のセッションで保護 (Sign in with Slack / OIDC。Cloudflare Access は使わない)
noisy-crow-events (apps/noisy-crow-events)素の Cloudflare Worker (fetch ハンドラ)Slack Events API の受信 (/slack/events) → 署名検証 → 即 200 → ctx.waitUntil で共有コアの processStampRequest を実行し Slack reactions.add まで行う。ユーザー認証を持たない公開 Worker で、真正性は Slack 署名で担保
Slack AppHTTP Events API (message.channels) を events Worker (https://nc-events.tied-workspace.com/slack/events) に POST する。Socket Mode は無効

構成の経緯: 以前は「Hono の REST API Worker (nc-api.tied-workspace.com)」と「Vite + React の SPA を Cloudflare Pages (nc.tied-workspace.com)」に分けており、両者を CORS / credentials: "include" / クロスサブドメイン Access cookie / Vite proxy で繋ぎ、API レスポンス型を SPA 側 lib/api.ts と 1:1 で手動同期していた。これを React Router v7 の framework モード (SSR on Workers) に統合し、loader / action がサーバー (Worker) 上で共有コアのユースケースを直接呼ぶ構成にした。これにより別ドメイン・CORS・fetch ラッパ・型の二重管理が不要になった。さらに旧構成では Slack Socket Mode の Bot Container がメッセージを Cloudflare Queue (noisy-crow-stamp-requests + DLQ) に投入し、Queue Consumer Worker が処理していたが、これらの Bot Container / Consumer Worker / Queue / Socket Mode は全て廃止済み (旧構成)。現在は Slack HTTP Events API を専用の公開 Worker noisy-crow-events で受け、署名検証 → 即 200 → ctx.waitUntil(processStampRequest) という非同期処理に置き換えている。データアクセス層は packages/noisy-crow-core に共通化し、UI Worker と events Worker の両方が共有する。

詳細は以下のサブページに分割しています。

設計の不変条件 (Invariants)

これらは「変えてはいけない方針」です。変更が必要な場合はまずこのドキュメントを更新し、レビューで合意してから着手してください。

  1. クリーンアーキテクチャを崩さない: domainapplicationinfrastructure の依存方向を保つ。application は Cloudflare API を直接知らない。infrastructure の差し替えで Bun / Workers どれでも動かせる状態を保つ。共有コア (packages/noisy-crow-core) は framework 非依存であり、両 Worker から import される。
  2. 認証ロジックはアプリ層に書かない (共有認証基盤に集約): 認証の実装は共有認証基盤 packages/auth (better-auth + 共有 D1 tied-auth) のみに置き、noisy-crow 側には instantiation と requireSession の薄い配線しか持たない。
    • UI Worker (noisy-crow-web) は better-auth のセッションで保護する (IdP は Sign in with Slack / OIDC、AUTH_DB = 共有 D1 tied-auth)。Cloudflare Access は使わない — やまびこ (yamabiko-web) に次ぐ意図的な例外。各ページの loader / action 冒頭で requireSession を呼び (GET/POST 両方)、/sign-in/api/auth/* のみ保護対象外。workers_dev = false / preview_urls = false で workers.dev / preview URL の素通りを塞ぐ。
    • 限定的な例外 (events Worker): /slack/events を受ける noisy-crow-events は Slack がユーザー認証を通過できないため ユーザー認証を持たない公開 Worker とする。この公開 Worker のリクエスト真正性は Slack 署名検証 (SLACK_SIGNING_SECRET / HMAC-SHA256、src/verify-signature.ts) で担保する。これはユーザー認証ではなくリクエスト検証であり、本不変条件に対する意図的かつスコープを限定した例外である。
  3. イベントの重複処理を防ぐ: Slack Events API は同一イベントを複数回配送しうる。重複排除は events Worker の processStampRequest 内で SESSIONS_KVevent:<eventId> を確認して行う。旧構成の「Bot が enqueue + Consumer が processing」という 2 段の責務分担は廃止され、events Worker 内の processStampRequest 1 箇所で冪等性を担保する。Queue を廃止したため自動リトライ / DLQ は無く、重複は SESSIONS_KV で抑止する (簡素さを優先した意図的なトレードオフ)。
  4. AI 呼び出しは AI Gateway 経由: Google 直接の SDK 呼び出しを足さない。レート制御 / 観測性を Cloudflare AI Gateway に集約する。利用するプロバイダーは AI Gateway がサポートするものに限る (旧実装の Voyage は AI Gateway 非対応プロバイダーのため error 2008 Invalid provider で全滅した。Gemini embedding に移行済み)。認証は GOOGLE_API_KEY があれば全リクエストに x-goog-api-key を付与 (リクエスト自体は Gateway 経由のまま)。Unified Billing の認証注入はサポート対象のモデル × エンドポイントに限られ、本リポジトリが使う全ての組み合わせ (Batch 系 / gemini-3.5-flash の generateContent / gemini-embedding-001 の embedContent) で注入されず 403 になることを本番確認済み (2026-06)。このため GOOGLE_API_KEY は両 Worker で必須 (キー未設定時の Unified Billing フォールバックは実装上の保険にすぎない)。
  5. ブラウザから Cloudflare リソースを直接触らない: データ操作は必ず RR7 の loader / action (Worker 上で実行されるサーバーコード) を経由する。ブラウザに配信されるクライアントコードから KV / R2 / Vectorize / Slack を直接叩いてはいけない。Cloudflare バインディングに触れるのは *.server.ts と loader / action だけ。
  6. TypeScript の相対 import に拡張子を付けない: 本リポジトリは moduleResolution: "bundler" 統一。./foo.js ではなく ./foo
  7. 絵文字インデックス同期を HTTP リクエスト内で実行しない: 同期は絵文字数に比例する長時間バッチであり、loader / action / ctx.waitUntil では完走しない。必ず Workflow noisy-crow-sync を起動する形にする (action は起動と進捗参照のみ)。AI 呼び出しは絵文字 1 件ごとではなく バッチ推論 (caption は Gemini Batch API / embedding は Gemini batchEmbedContents) でまとめる。メタデータには生成時のモデル名を記録し、モデル更新時に rebuild モードで差分再構築できる状態を保つ。
    • 限定的な例外 (個別更新): 絵文字 1 件だけの caption 修正 / 再解析 (updateEmojiCaption / reanalyzeEmoji) は定数時間 (caption 1 件 + embedding 1 件) で終わるため action 内で同期実行してよい。sync ジョブ実行中は拒否する。手動編集した caption は captionModel: "manual" で記録し、rebuild でも再生成せず維持する。