コンテンツにスキップ

ストレージレイヤー

v0.7.0のストレージレイヤー(frontend/src/storage/)は、フロントエンドに単一の契約の背後に2つの交換可能なバックエンドを提供します。この契約はPhase 7〜34を経て22の名前空間に拡大しています。

IStorageService

frontend/src/storage/types.tsはすべてのストレージ実装が満たすインターフェースを定義します。api/client.tsapi.*名前空間を1:1でミラーリングしています。

export interface IStorageService {
  readonly mode: StorageMode;
  health(): Promise<HealthInfo>;
  // Core
  i18n: II18nNamespace;
  users: IUsersNamespace;
  projects: IProjectsNamespace;
  settings: ISettingsNamespace;   // get/set including key_source_*
  assessment: IAssessmentNamespace;
  session: ISessionNamespace;     // includes streamMessage()
  tracking: ITrackingNamespace;
  tools: IToolsNamespace;
  curricula: ICurriculaNamespace;
  topics: ITopicsNamespace;
  lessons: ILessonsNamespace;
  plugins: IPluginsNamespace;
  system: ISystemNamespace;
  // Phase 12+
  backup: IBackupNamespace;
  export: IExportNamespace;
  imports: IImportsNamespace;
  // Phase 22 — taxonomy
  subjects: ISubjectsNamespace;
  tags: ITagsNamespace;
  projectTaxonomy: IProjectTaxonomyNamespace;
  // Phase 29-32 — gamification + exports
  gamification: IGamificationNamespace;
  anki: IAnkiNamespace;
  notebooklm: INotebookLmNamespace;
  pronunciation: IPronunciationNamespace;
}

すべてのページはgetStorage()ファクトリを通じてIStorageServiceを利用します。ページはapi/client.tsやDexieデータベースを直接インポートしません。

ApiStorage

storage/api-storage.tsapi.*への薄いパススルーです。すべてのメソッドが1:1で委譲します。動作はv0.6.0と同一です。

DexieStorage

storage/dexie-storage.tsはDexie 4.4.2を通じてすべてをIndexedDBに永続化します。storage/db.tsのスキーマはSQLAlchemyの25モデルすべてを1:1でミラーリングし、4つの関連テーブル(project_subjects / project_tags / など)も含みます。

storage/以下のサブモジュールがポートされたロジックを担います。

モジュール 役割
assessment.ts 12問パック + プロファイル計算機
prompts.ts 42セルのシステムプロンプトマトリクス
step-evaluator.ts デュアルプロンプトのステップ評価ポート
session-flow.ts start + messageのオーケストレーション
tracking.ts アグリゲーター + buildCommitFromSession
tools.ts rankTools + buildSpacedRecommendations
ai-providers.ts Anthropic/OpenAI/Gemini HTTPクライアント

バンドルデータはfrontend/src/data/に格納されます。

  • assessment-questions.json — バックエンドのQUESTIONSリストからそのままエクスポート(12問 × 4回答 × 5言語)。
  • session-prompts.json — バックエンドの_PROMPTS辞書からそのままエクスポート(6メソッド × 7ステップ × 2言語)。

3番目のストレージバックエンドの追加

任意の永続化レイヤー(Supabase、Firestore、カスタムREST APIなど)でIStorageServiceを実装します。storage/index.tsのファクトリに登録します。

if (mode === "supabase") {
  cachedStorage = supabaseStorage;
}

StorageMode型にモードを追加します。

export type StorageMode = "api" | "dexie" | "supabase";

Settings UIのストレージモードセクションに配線します。他のファイルは変更不要です。ページは引き続きgetStorage()を通じてアクセスします。

ブラウザ直接AIコール

storage/ai-providers.tsは3つのプロバイダークライアントを実装しています。

  • Anthropicanthropic-dangerous-direct-browser-access: trueヘッダーを付けてhttps://api.anthropic.com/v1/messagesにPOST。これはAnthropicのブラウザ呼び出し元向けの明示的なオプトインです。このヘッダーがないとCORSが拒否します。
  • OpenAIAuthorization: Bearer ${apiKey}を付けてhttps://api.openai.com/v1/chat/completionsにPOST。CORSはデフォルトで開放されています。
  • Geminihttps://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={apiKey}にPOST。クエリパラメーター認証で、systemフィールドなし。systemメッセージは最初のユーザーターンに折り込まれます。

3つすべてがエラーをApiError(status, "Provider: detail")に正規化するため、既存のフロントエンドトースト / GitHub Issue UXが分岐なしにレンダリングできます。

Dexieモードでなぜ平文のAPIキーなのか?

DexieモードではユーザーのAPIキーはIndexedDBに平文で格納されます(UserSettings.api_key_{provider})。許容できる脅威モデル:

  • データはユーザー自身のデバイスから外に出ません。
  • AIプロバイダーのみがキーを見るネットワークエンドポイントです。
  • IndexedDBでの暗号化には、セッションごとのパスワードプロンプト(UX的に不親切)か、アプリにバンドルされた固定キー(攻撃者がバンドルを持っているのでセキュリティ上の見せかけ)が必要になります。

サーバーモードの動作は異なります。APIキーはFernet暗号化で保存されます(ADAPTIVE_LEARNER_SECRET_KEY)。ApiStorageは平文を見ません。

v1.20.0 / Phase 34以降、両モードともプロバイダーごとのソース属性(UserSettings.key_source_anthropic | openai | gemini)を公開し、UIが「Key from: secrets.yaml」/「environment」/「Settings」とレンダリングできるようにしています。Dexieモードではブラウザのサンドボックスにはファイルシステムアクセスがないため、ソースはsettingsまたはnoneに縮退します(secrets.yamlはデスクトップ / サーバーモードの概念です)。

モード解決

storage/index.tsは次の順序でモードを解決します。

  1. localStorage["adaptive-learner.storage_mode"] — Settingsでのユーザー選択。
  2. VITE_STORAGE_MODE — ビルド時のデフォルト(GH Pagesでは"dexie"に設定)。
  3. フォールバック: "api"

結果はページのライフタイム中キャッシュされます。テストコードは_resetStorageCacheForTests()でリセットできます。