システム構成
MTS Translation Bot は、役割の異なる複数のプロセスが連携して動作します。このページでは技術的な全体像を説明します(運用に必須の知識ではありません)。
構成要素
┌──────────────────────────┐
Discord ◀────────▶│ Bot 本体 (discord.js) │
ゲートウェイ │ シャーディングで分散実行 │
└─────────────┬────────────┘
│ 直接読み書き
ブラウザ ─OAuth─┐ ▼
│ ┌────────────────┐
┌──────────────▼───────┐ │ PostgreSQL │
│ Web ダッシュボード │ │ (設定/課金/統計) │
│ (Next.js) │ └────────▲───────┘
└──────────┬───────────┘ │ 直接読み書き
│ 内部API (認証付き) │
▼ │
┌──────────────────────────────┴──┐
│ internal-api (Express) │
│ ダッシュボード向けの読み書き窓口 │
└──────────────────────────────────┘| コンポーネント | 役割 |
|---|---|
| Bot 本体 | Discord と接続し、発言の取得・翻訳・Webhook投稿・リアクション/編集/削除の処理を行う中核。 |
| internal-api | ダッシュボードからの設定変更・情報取得を受け付ける内部APIサーバー。認証付きで、外部公開はされません。 |
| ダッシュボード | 管理者が使う Web UI。internal-api 経由でデータを読み書きします。 |
| PostgreSQL | 設定・課金・利用統計などを保存するデータベース。Bot と internal-api が共有します。 |
| 音声認識サーバー | VC文字起こし用の Whisper を常駐させたローカルサービス。Bot からHTTPで呼び出します。任意(VC機能を使う場合のみ)。 |
| 読み上げサーバー | VOICEVOX / Piper をまとめたローカルサービス。任意(読み上げを使う場合のみ)。 |
音声系の2つはBotとは独立したプロセスです。停止していてもBotは通常どおり動作し、VC機能が使えなくなるだけです。
翻訳の処理フロー
- 受信:対象チャンネルの発言(または国旗リアクション)を検知。
- フィルタ:除外設定(ユーザー/ロール/キーワード/コードブロック)を適用。
- 整形:メンション・絵文字・チャンネルリンク・URL をプレースホルダーで保護。
- エンジン選択:モード(LocalLLM / DeepL)と残量・上限に応じて翻訳先エンジンを決定。
- 翻訳:選択エンジンで翻訳。失敗・過負荷時はフォールバックで別エンジンへ。
- 復元・整形:保護したトークンを元に戻し、改行構造を可能な限り復元。
- 投稿:Webhook で、発言者の名前/アイコンを再現して翻訳結果を投稿。
- 記録:元メッセージと翻訳投稿の対応関係を記録(編集/削除の追従に使用)。利用回数を集計。
シャーディング
Discord の大規模運用に対応するため、Bot はシャーディング(接続を複数のワーカーに分割)で動作します。これにより、多数のサーバーに参加していても安定して稼働します。
ストレージ
すべての永続データは PostgreSQL に集約されます。
保存される主なもの:サーバー/ユーザー設定、課金・権利情報、利用回数の集計、メッセージ対応関係 など。 メッセージ本文とVCの音声は恒久保存しません(→ データの取り扱い)。
データの持ち方
かつては用途ごとに1つの大きな JSON ドキュメントへまとめて保存していましたが、規模の拡大に伴いエンティティ単位の行へ移行しました。
| データ | 現在の持ち方 |
|---|---|
| サーバー設定 / ユーザー設定 | 1件1行 |
| DeepL残高・購入台帳 | 1件1行(残高と台帳を単一トランザクションで更新) |
| 日次の利用統計 | カウンタ1つにつき1行 |
| メッセージ対応関係 | 1件1行(保持期間を過ぎたものは自動削除) |
これにより、翻訳1回あたりの読み書き量がドキュメント全体から該当行のみに縮小しています。 また、金額に関わる更新はデータベースの制約(一意キー・CHECK)で保証する構成にしており、二重加算や残高のマイナスをアプリケーションのロジックだけに頼らない形にしています。
決済連携
- 課金は Stripe を利用します。
- 購入はまず「保留(pending)」として作成され、Stripe の Webhook で決済確定後に「承認(approved)」へ。
- 失敗・期限切れは自動で「却下」、月額は請求書/サブスクイベントに応じて状態が更新されます。
Checkout 自体の二重作成を防ぐ仕組み
- 購入ボタンを押すと、ブラウザーがその購入操作専用の
checkoutRequestIdを1つ生成します。通信エラーや二重クリックで再送しても同じIDを再利用します。 - PostgreSQL の
stripe_checkout_requestsは(user_id, request_id)を主キーにし、同じ操作へ必ず同じ購入IDを割り当てます。 - Stripe Checkout APIにも、その操作・ユーザー・商品種別から作った同じ Idempotency Key を渡します。Dashboardが複数プロセスでも、同じ操作から複数のCheckout Sessionを作りません。
- LocalLLMプランは購入ストアのCAS更新内でもサーバー単位のpendingを排他し、別タブが別request IDを同時生成した場合も1件だけ受け付けます。
- すでにSession IDが購入記録にある再送では、新規Sessionを作らずStripeから既存Sessionを取得して返します。取得に失敗した場合は安全側に倒し、新しい決済を作りません。
「払ったのに反映されない/二重反映」を防ぐ多層設計
決済の取りこぼし・欠損・二重加算を防ぐため、3層で確実に反映します。
- Webリンク(Webhook):決済確定を Stripe が通知。失敗時は Stripe が最大数日リトライ。
- 成功画面での再確認(reconcile):購入後に戻った画面で、その場で購入を照合・反映(Webhook遅延の保険)。
- 定期照合(sweep):保留中の購入を定期的に Stripe へ問い合わせて反映(通知が届かず・画面にも戻らなかった場合の最終保険、内部用エンドポイントをcronで実行)。
さらに、反映処理は冪等です。同じ購入が複数経路(Webhook・再確認・定期照合)で重複処理されても、残高や権利は一度だけ反映されます(購入IDで重複を排除)。これにより「二重に付与される/欠損する」ことを防いでいます。
また、サブスクリプションの状態更新は イベントの発生時刻(event.created)で順序を保証します。Stripe の通知は到着順が前後することがあるため、古い通知(例:解約)が新しい状態(例:更新成功)を上書きしないようにしています(比較はストアのロック内で実施=アトミック)。
信頼性の工夫
- 多段フォールバックで翻訳の途切れを防止(→ 翻訳エンジンとフォールバック)。
- 利用上限でサーバー間の公平性とリソース保護を両立(→ 利用上限とレート)。
- 設定変更は内部APIを通すため、ダッシュボードと Bot の設定が一貫します。
このページの内容は概念的な説明です。実際の構成・パラメータは運用状況により調整されます。
