Skip to content

データ層 (loader / action)

旧 Hono REST API Worker は廃止し (旧構成)、ブラウザ向けのデータ操作は UI Worker noisy-crow-web (apps/noisy-crow-app, React Router v7 framework モード) の loader / action に統合した。loader / action は Cloudflare Worker 上 (サーバー) で実行され、context.cloudflare.env のバインディングを buildWebDeps(env) に渡して共有コア @tied-workspace/noisy-crow-core のユースケースを直接呼ぶ。

Slack Events API の受信 (/slack/events) は UI Worker には無く、専用の公開 Worker noisy-crow-events (apps/noisy-crow-events) に分離されている (下記「/slack/events (Slack Events ingestion)」参照)。

ブラウザは独自の fetch ラッパや REST エンドポイントを持たず、RR7 の <Form> / useFetcher / ナビゲーションを通じて loader / action を呼ぶ。したがって API レスポンス型を別途定義する必要がなく、型は共有コアの戻り値がそのまま正本になる。

認証 / ドメイン

  • UI Worker の本番ドメインは https://nc.tied-workspace.com (better-auth セッションで保護。IdP は Sign in with Slack / OIDC)。別ドメインの API (nc-api) は無くなった (旧構成)。
  • 同一オリジン内で完結するため CORS / credentials: "include" は不要
  • 認証ロジックはアプリに持たず共有認証基盤 @tied-workspace/auth に集約する。各 loader / action の冒頭で requireSession(request, env) を呼んで未サインインを /sign-in へ弾く (GET/POST 両方)。/sign-in と better-auth ハンドラ /api/auth/* のみ保護対象外。

ルートとデータ操作の対応 (UI Worker noisy-crow-web)

ルート種別呼ぶユースケース / 処理返却
/ (index)loader/emojis へ redirect (Home / Setup は廃止)
/emojis (emojis)loaderlistEmojis({ page, perPage: 60, filter }) (URL クエリ ?q&page、ページングとフィルタは D1 側){ emojis, excluded, total, page, perPage, q }
/emojis (emojis)action intent=excludeexcludeEmoji({ name, url }){ ok: true }
/emojis (emojis)action intent=includeincludeEmoji(name){ ok: true }
/syncs (syncs)loadergetSyncStatus() + listSyncHistory({ page, perPage: 20 }) (URL クエリ ?page){ syncStatus, history }
/syncs (syncs)action intent=syncstartEmojiSync(deps, "diff") — Workflow noisy-crow-sync を起動して即返す{ sync: { started, status } }
/syncs (syncs)action intent=rebuildstartEmojiSync(deps, "rebuild") — モデル更新後の再構築{ sync: { started, status } }
/emojis/:name (emoji-detail)loaderdeps.vectors.get(name) + getSyncStatus() (見つからなければ 404){ emoji, syncStatus }
/emojis/:name (emoji-detail)action intent=update-captionupdateEmojiCaption({ name, description }) — 手動 caption で再 embedding (captionModel: "manual"){ ok: true, description }
/emojis/:name (emoji-detail)action intent=reanalyzereanalyzeEmoji({ name, extraInstructions? }) — Gemini 単件で caption 再生成 + 再 embedding{ ok: true, description }
/search (search)loadersearchEmojis({ query, limit, threshold }) (URL クエリ ?q&limit&threshold){ query, limit, threshold, results }
/visualization (visualization)action intent=generatevisualizeEmojis({ method, queries, clusterCount }) — サーバー側で投影 (PCA / t-SNE) + 事前/事後クラスタリング (k-means) + AI クラスタラベル生成まで実行{ kind: "visualization", data: EmojiVisualization }

/slack/events は UI Worker のルートではない。Slack Events 受信は別 Worker noisy-crow-events が担う (下記参照)。

  • /emojis の loader は 1 ページ分 (60 件) だけ 返す。全件をシリアライズするとインデックス件数に比例して遅くなるため、ページングとフィルタ (name / description の LIKE) は D1 emojis テーブル側で行う (EmojiVectorStore.listPage)。emojis[].vector は空配列。ベクトルが必要なのは /visualizationintent=generate (listWithVectors()) だけで、サーバー側でのみ使われる。
  • 同期は action 内で実行しない/syncsintent=sync / intent=rebuild は Workflow を起動して即返し、進捗は loader の syncStatus (D1 sync_jobs の最新行) を実行中のみ 3 秒間隔で再検証して表示する。実行中ジョブがある場合 started: false が返り二重起動しない。/syncs の loader / action は buildSyncDeps(env) (= buildWebDeps + SYNC_WORKFLOW バインディング) を使う。過去ジョブは D1 sync_jobs の履歴 (listSyncHistory) で参照する。
  • 個別更新 (/emojis/:nameupdate-caption / reanalyze) は例外的に action 内で同期実行する。1 件あたり caption 1 リクエスト + embedding 1 リクエストの定数時間で終わるため。sync ジョブの実行中はユースケース側が拒否し { error } が返る。action は vector (1024 次元) を返さず、loader の再検証で表示を更新する。
  • action はエラーを try/catch し、{ error } または { kind: "error", message } を返してページ側でインライン表示する (例外を投げると ErrorBoundary に飛ぶ)。
  • /visualizationintent=generatePCA / t-SNE の投影・k-means クラスタリング・AI ラベル生成をすべてサーバー側 (action) で実行 し、1024 次元ベクトルはブラウザに送らない (返るのは 2D 座標 + クラスタ情報のみ)。query 点の embedding も generate 時にサーバーが生成する。CPU バウンドな処理のため UI Worker は limits.cpu_ms: 300000 を設定している (t-SNE は 2000 点超で PCA にフォールバック)。AI ラベル生成 (AiGateway.generateClusterLabels) の失敗は警告に落とし、可視化自体は返す。

/slack/events (Slack Events ingestion) — Events Worker noisy-crow-events

ブラウザ向けの UI ルートではなく、Slack の HTTP Events API が POST するサーバーサイド・エンドポイントUI Worker (noisy-crow-web) ではなく、専用の公開 Worker noisy-crow-events (apps/noisy-crow-events, 素の fetch ハンドラ src/index.ts) が受ける。

  • まず src/verify-signature.tsSlack 署名検証 を行う (SLACK_SIGNING_SECRET を使った v0:{timestamp}:{body} の HMAC-SHA256、5 分のリプレイ窓)。検証に失敗したリクエストは拒否する。url_verification チャレンジにも対応する。
  • 検証を通ったら 即 200 を返す (Slack の 3 秒 ack 制約)。実処理は ctx.waitUntil 内で非同期に走らせ、buildProcessStampDeps(env) で組み立てた deps を共有コアの processStampRequest に渡す。
  • Queue は廃止されたため自動リトライ / DLQ は無い (旧構成)。重複配送は processStampRequest 内の SESSIONS_KV (event:<id>) 重複排除で吸収する。
  • noisy-crow-events は Slack がユーザー認証 (better-auth) を通過できないため ユーザー認証を持たない公開 Worker とする。リクエスト真正性は上記 Slack 署名検証で担保する (ユーザー認証ではなくリクエスト検証)。UI Worker (better-auth で保護) とは別 Worker のため、UI 側に署名検証用の穴を開ける必要は無い。

不変条件

  • 新しいデータ操作を足すときは、必ず 共有コア @tied-workspace/noisy-crow-core のユースケース関数 (または ports 実装) を経由 する。loader / action 内で Cloudflare バインディングを生で叩かない。buildWebDeps(env) が組み立てた deps 越しに呼ぶ。
  • Cloudflare リソースに触れるのは *.server.ts と loader / action のみ。ブラウザに配信されるコードに混ぜない (.server.ts 命名で物理的に分離する)。
  • 独自の認証 / 認可ロジックは追加しない。認証は共有認証基盤 @tied-workspace/auth に集約し、ページは requireSession を呼ぶだけにとどめる。
  • 型は共有コア (@tied-workspace/noisy-crow-coredomain/emojiEmojiVector / ExcludedEmoji、各ユースケースの戻り値) を import して使う。ダッシュボード側で再定義しない。