データ層 (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) | loader | listEmojis({ page, perPage: 60, filter }) (URL クエリ ?q&page、ページングとフィルタは D1 側) | { emojis, excluded, total, page, perPage, q } |
/emojis (emojis) | action intent=exclude | excludeEmoji({ name, url }) | { ok: true } |
/emojis (emojis) | action intent=include | includeEmoji(name) | { ok: true } |
/syncs (syncs) | loader | getSyncStatus() + listSyncHistory({ page, perPage: 20 }) (URL クエリ ?page) | { syncStatus, history } |
/syncs (syncs) | action intent=sync | startEmojiSync(deps, "diff") — Workflow noisy-crow-sync を起動して即返す | { sync: { started, status } } |
/syncs (syncs) | action intent=rebuild | startEmojiSync(deps, "rebuild") — モデル更新後の再構築 | { sync: { started, status } } |
/emojis/:name (emoji-detail) | loader | deps.vectors.get(name) + getSyncStatus() (見つからなければ 404) | { emoji, syncStatus } |
/emojis/:name (emoji-detail) | action intent=update-caption | updateEmojiCaption({ name, description }) — 手動 caption で再 embedding (captionModel: "manual") | { ok: true, description } |
/emojis/:name (emoji-detail) | action intent=reanalyze | reanalyzeEmoji({ name, extraInstructions? }) — Gemini 単件で caption 再生成 + 再 embedding | { ok: true, description } |
/search (search) | loader | searchEmojis({ query, limit, threshold }) (URL クエリ ?q&limit&threshold) | { query, limit, threshold, results } |
/visualization (visualization) | action intent=generate | visualizeEmojis({ 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) は D1emojisテーブル側で行う (EmojiVectorStore.listPage)。emojis[].vectorは空配列。ベクトルが必要なのは/visualizationのintent=generate(listWithVectors()) だけで、サーバー側でのみ使われる。- 同期は action 内で実行しない。
/syncsのintent=sync/intent=rebuildは Workflow を起動して即返し、進捗は loader のsyncStatus(D1sync_jobsの最新行) を実行中のみ 3 秒間隔で再検証して表示する。実行中ジョブがある場合started: falseが返り二重起動しない。/syncsの loader / action はbuildSyncDeps(env)(=buildWebDeps+SYNC_WORKFLOWバインディング) を使う。過去ジョブは D1sync_jobsの履歴 (listSyncHistory) で参照する。 - 個別更新 (
/emojis/:nameのupdate-caption/reanalyze) は例外的に action 内で同期実行する。1 件あたり caption 1 リクエスト + embedding 1 リクエストの定数時間で終わるため。sync ジョブの実行中はユースケース側が拒否し{ error }が返る。action は vector (1024 次元) を返さず、loader の再検証で表示を更新する。 - action はエラーを
try/catchし、{ error }または{ kind: "error", message }を返してページ側でインライン表示する (例外を投げるとErrorBoundaryに飛ぶ)。 /visualizationのintent=generateは PCA / 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.tsで Slack 署名検証 を行う (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-coreのdomain/emojiのEmojiVector/ExcludedEmoji、各ユースケースの戻り値) を import して使う。ダッシュボード側で再定義しない。