ダミ声カラス (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 App | — | HTTP 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 を専用の公開 Workernoisy-crow-eventsで受け、署名検証 → 即 200 →ctx.waitUntil(processStampRequest)という非同期処理に置き換えている。データアクセス層はpackages/noisy-crow-coreに共通化し、UI Worker と events Worker の両方が共有する。
詳細は以下のサブページに分割しています。
- アーキテクチャ — レイヤー構造、データフロー、配置
- ドメインとユースケース — エンティティ、ports、ユースケース関数
- Cloudflare リソース — KV / D1 / R2 / Vectorize / AI Gateway
- データ層 (loader / action) — UI Worker がブラウザに提供するデータ操作と、events Worker の
/slack/events受信 - Web (Dashboard) — UI ページと操作仕様 (RR7 framework モード)
- 運用 / デプロイ — ローカル開発、デプロイ手順、設定値
設計の不変条件 (Invariants)
これらは「変えてはいけない方針」です。変更が必要な場合はまずこのドキュメントを更新し、レビューで合意してから着手してください。
- クリーンアーキテクチャを崩さない:
domain→application→infrastructureの依存方向を保つ。applicationは Cloudflare API を直接知らない。infrastructureの差し替えで Bun / Workers どれでも動かせる状態を保つ。共有コア (packages/noisy-crow-core) は framework 非依存であり、両 Worker から import される。 - 認証ロジックはアプリ層に書かない (共有認証基盤に集約): 認証の実装は共有認証基盤
packages/auth(better-auth + 共有 D1tied-auth) のみに置き、noisy-crow 側には instantiation とrequireSessionの薄い配線しか持たない。- UI Worker (
noisy-crow-web) は better-auth のセッションで保護する (IdP は Sign in with Slack / OIDC、AUTH_DB= 共有 D1tied-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) で担保する。これはユーザー認証ではなくリクエスト検証であり、本不変条件に対する意図的かつスコープを限定した例外である。
- UI Worker (
- イベントの重複処理を防ぐ: Slack Events API は同一イベントを複数回配送しうる。重複排除は events Worker の
processStampRequest内でSESSIONS_KVのevent:<eventId>を確認して行う。旧構成の「Bot が enqueue + Consumer が processing」という 2 段の責務分担は廃止され、events Worker 内のprocessStampRequest1 箇所で冪等性を担保する。Queue を廃止したため自動リトライ / DLQ は無く、重複は SESSIONS_KV で抑止する (簡素さを優先した意図的なトレードオフ)。 - 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 フォールバックは実装上の保険にすぎない)。 - ブラウザから Cloudflare リソースを直接触らない: データ操作は必ず RR7 の loader / action (Worker 上で実行されるサーバーコード) を経由する。ブラウザに配信されるクライアントコードから KV / R2 / Vectorize / Slack を直接叩いてはいけない。Cloudflare バインディングに触れるのは
*.server.tsと loader / action だけ。 - TypeScript の相対 import に拡張子を付けない: 本リポジトリは
moduleResolution: "bundler"統一。./foo.jsではなく./foo。 - 絵文字インデックス同期を HTTP リクエスト内で実行しない: 同期は絵文字数に比例する長時間バッチであり、loader / action /
ctx.waitUntilでは完走しない。必ず Workflownoisy-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 でも再生成せず維持する。
- 限定的な例外 (個別更新): 絵文字 1 件だけの caption 修正 / 再解析 (