API仕様書
Honoフレームワーク基盤のAPIエンドポイント仕様
API仕様書 (05_api_specification.md)
本ドキュメントは、Hono フレームワークに集約された新 API 構造に基づいて記載されています。
1. 概要
本アプリケーションのバックエンドは、Next.jsのAPI Routes機能を利用して構築されています。これにより、フロントエンドと同じプロジェクト内でサーバーサイドロジックを管理し、特に外部のLLM(大規模言語モデル)プロバイダーとの連携を担います。
- 設計思想:
- API RoutesはBFF (Backend For Frontend) として機能し、クライアントからのリクエストを整形して各種LLMプロバイダーへ仲介します。
- APIキーなどの機密情報は、原則としてサーバーサイド(環境変数)で管理しますが、ユーザーがクライアントサイドで入力したキーを優先的に使用する「Bring Your Own Key (BYOK)」モデルもサポートします。
2. 内部API (/api/*)
フロントエンドとバックエンドの間の通信に使用されるRPC風の内部APIです。
| エンドポイント | HTTPメソッド | 主な役割 | 分類 (サブアプリ) |
|---|---|---|---|
/api/chat | POST | 汎用的なAIとの対話、エージェント実行 | chat.ts |
/api/ai/review | POST | 作品原稿の査読レポート生成 | ai.ts |
/api/ai/metadata-generate | POST | 作品のメタデータ案(タイトル等)生成 | ai.ts |
/api/ai/metadata-translate | POST | メタデータの多言語翻訳 | ai.ts |
/api/ai/structure-plot | POST | プロットの構造化 | ai.ts |
/api/ai/refine-plot-chunk | POST | プロットアイテムの改修 | ai.ts |
/api/publish | POST | 作品のクラウド公開 | publish.ts |
/api/projects/status | POST | プロジェクトの公開・非公開状態管理 | projects.ts |
/api/library | GET/POST | 本棚(ライブラリ)のアイテム取得・操作 | library.ts |
/api/tools/web-search | POST | Web検索のプロキシ | tools.ts |
/api/tools/open-url | POST | URLの読込とMarkdown変換 | tools.ts |
/api/tools/adit | POST | AIによる決定論的テキスト編集 (Adit) | tools.ts |
/api/models/openrouter | GET | OpenRouter のモデル一覧取得 | models.ts |
/api/current-time | GET | サーバー時刻の取得 | infra.ts |
/api/seo/sitemap | GET | サイトマップの動的生成 | seo.ts |
3. RESTful API (/v1/*)
RESTful規約に準拠した外部公開APIです。API Key認証が必須です。
3.1. 認証方式
API Key認証: Authorization: Bearer <key> ヘッダーによる認証
- 発行: Settings画面の「Developer API Keys」セクションから発行可能
- 権限スコープ:
["read"](読み取り専用)、["read", "write"](読み書き) - レートリミット: API Key単位のリクエスト制限(read: 300/hour, write: 60/hour)
3.2. Public API(読み取り専用)
| エンドポイント | HTTPメソッド | 説明 | クエリパラメータ |
|---|---|---|---|
/v1/projects | GET | 公開プロジェクト一覧 | page, q, tags |
/v1/projects/:id | GET | プロジェクト詳細(profiles JOIN付き) | - |
/v1/projects/:id/content | GET | 原稿内容 | lang(言語絞り) |
/v1/projects/:id/toc | GET | 目次(story ID + 文字数 + 対応言語) | - |
/v1/projects/:id/stories/:storyId | GET | 1話分の本文 | lang |
/v1/users/:id | GET | ユーザープロフィール(公開情報のみ) | - |
/v1/tags | GET | ローカライズ済みタグ一覧 | lang |
3.3. Developer API(認証付き / 管理操作)
| エンドポイント | HTTPメソッド | 説明 | 必要権限 |
|---|---|---|---|
/v1/users/me | GET | 認証済みユーザー情報 | - |
/v1/users/me/projects | GET | 自分の出版プロジェクト一覧 | - |
/v1/users/me/library | GET | ライブラリ一覧 | - |
/v1/projects/:id | DELETE | 出版プロジェクトの非公開化 | write |
/v1/projects/:id | PATCH | 出版済みプロジェクトのメタデータ更新 | write |
3.4. OpenAPI・Scalar UI
- OpenAPI仕様書:
/v1/openapi.json— @hono/zod-openapi で自動生成 - APIドキュメント:
/v1/docs— Scalar UI で表示(認証テスト可能)
4. エンドポイント仕様詳細
3.1. GET /api/projects/[id]/metadata
指定されたprojectIdの公開済みメタデータ(title, synopsis, tagsなど)をSupabaseから取得します。
- 目的: 公開済みプロジェクトを更新する際に、既存のメタデータをフォームの初期値として表示するために使用します。
- 認証: 不要 (RLSにより公開データのみ返却されるため)
- リクエストパラメータ (
params):id(必須):string。取得するプロジェクトのUUID。
- 処理フロー:
projectIdが存在しない場合は400 Bad Requestを返します。published_projectsテーブルから、指定されたidのプロジェクトのメタデータ(title,headline,synopsis,original_language,default_language,localizations,cover_image_url)を取得します。- プロジェクトが見つからない場合(
PGRST116エラーコード)、nullと200 OKを返します。
- 成功レスポンス (200 OK):
{ "title": "作品タイトル(英語)", "headline": "キャッチコピー(英語)", "synopsis": "あらすじ(英語)", "original_language": "ja", "default_language": "en", "localizations": { "ja": { "title": "日本語タイトル", "headline": "...", "synopsis": "...", "tags": ["タグ1"] }, "en": { "title": "English Title", "tags": ["tag1", "tag2"] } }, "cover_image_url": "https://..." }- プロジェクトが見つからない場合は
null。
- プロジェクトが見つからない場合は
- エラーレスポンス:
- Status Code:
400 Bad Request,500 Internal Server Error - Body:
{ "error": "エラーの概要", "details": "詳細なエラーメッセージ" }
- Status Code:
3.2. PATCH /api/projects/[id]/metadata
指定されたprojectIdの公開済みメタデータを部分的に更新します。多言語対応(英語と原典言語)のメタデータ編集をサポートします。
- 目的: マイワークス画面の「メタデータを編集」モーダルから、タイトル、あらすじ、カバー画像などを更新する際に使用します。
- 認証: 必須。
auth.users.idがプロジェクトの所有者(user_id)と一致する場合のみ更新が許可されます。 - リクエストパラメータ (
params):id(必須):string。更新するプロジェクトのUUID。
- リクエストボディ (JSON):
{ "title": "作品タイトル(英語)", "headline": "キャッチコピー(英語)", // (任意) "synopsis": "あらすじ(英語)", // (任意) "localizations": { "ja": { "title": "日本語タイトル", "headline": "キャッチコピー", "synopsis": "あらすじ", "tags": ["タグ1"] }, "en": { "title": "English Title", "tags": ["tag1", "tag2"] } }, // (任意) localizations JSONBによる多言語メタデータ "cover_image_url": "https://..." // (任意) nullの場合は削除 } - 成功レスポンス (200 OK):
{ "message": "Metadata updated successfully" } - エラーレスポンス:
- Status Code:
400 Bad Request(バリデーションエラー),401 Unauthorized(未認証),404 Not Found(プロジェクトなし/権限なし),500 Internal Server Error - Body:
{ "error": "エラーメッセージ" }
- Status Code:
3.3. POST /api/publish
査読済みの作品をSupabaseのデータベースとストレージに保存し、公開状態にします。
- 目的: フロントエンドの公開ウィザードから送信された作品データを受け取り、永続化します。
- 認証: 必須。リクエストヘッダーのCookieを元にSupabaseのサーバーサイドクライアントでセッションを検証します。
- リクエストボディ (
FormData):file(任意):Fileオブジェクト。作品のカバー画像。data(必須):string。以下の構造を持つJSONオブジェクトを文字列化したもの。{ "id": "project-uuid-string", "title": "作品タイトル(英語フォールバック)", "headline": "キャッチコピー(英語フォールバック)", "synopsis": "あらすじ(英語フォールバック)", "original_language": "ja", // (任意) 原典言語コード "localizations": { "ja": { "title": "日本語タイトル", "headline": "日本語見出し", "synopsis": "日本語あらすじ", "tags": ["タグ1"] }, "en": { "title": "English Title", "headline": "English Headline", "synopsis": "English Synopsis", "tags": ["tag1"] } }, "content": { ... }, // ProjectContent型の完全なオブジェクト "review_report": { ... } // (任意) AI査読レポート }
- 処理フロー:
- ユーザーセッションを検証し、未認証の場合は
401 Unauthorizedを返します。 FormDataを解析します。dataフィールドが存在しない場合は400 Bad Requestを返します。- データ構造検証:
ProjectContentSchema(Zodスキーマ)によるランタイム検証と、構造整合性検証(孤立原稿・未執筆セクション・空原稿・空プロットの検出)を実行します。検証に失敗した場合は400 Bad Requestを返します。 fileが存在する場合、Supabase Storageのcoversバケットに{user_id}/{project_id}.{ext}のパスでアップロードし、公開URLを取得します。- データ順序の保証:
content.manuscriptsのキーをstructuredPlotの階層順序に基づいて並べ替えます(reorderManuscriptsByPlotヘルパー関数を使用)。これにより、JSON出力時のキー順序がプロット順になり、Reader ViewerやAPIレスポンスでの表示順序が保証されます。データ形式はオブジェクト{[sectionId]: {versions}}のまま変更しません。 published_projectsテーブルに、変換後のコンテンツと画像の公開URLをupsertします。
- ユーザーセッションを検証し、未認証の場合は
- 成功レスポンス (200 OK):
{ "success": true, "projectId": "project-uuid-string" } - エラーレスポンス:
- Status Code:
400 Bad Request,401 Unauthorized,500 Internal Server Error - Body:
{ "error": "エラーの概要", "details": "詳細なエラーメッセージ" } - バリデーションエラー時 (400 Bad Request):
{ "error": "Invalid project structure", "details": [ { "path": "structuredPlot", "message": "プロット構造が空です" } ], "warnings": [ { "path": "manuscripts.orphan-id", "message": "プロットに存在しないセクションIDの原稿です" } ] }
- Status Code:
3.2. POST /api/ai/review
作品原稿をAIが査読し、品質と安全性の評価レポートを生成します。レポートは常に英語で生成されます。
- 目的: 作品の公開前に、AIによる自動査読を行い、公開可否の判断材料を提供します。
- 認証: 必須。
- リクエストボディ (JSON):
{ "manuscripts": { ... }, // ProjectContent.manuscripts と同形式 "structuredPlot": { ... }, // StructuredPlot と同形式 "apiKey": "gemini-api-key" // ユーザーのGemini APIキー }manuscripts(必須): 査読対象の原稿データ。structuredPlot(必須): 原稿の構造化されたプロットデータ。apiKey(必須): Gemini APIキー。
- 処理フロー:
- 提供された原稿とプロットから、査読のためのコンテキストを生成します。
- Gemini AIモデルを呼び出し、査読レポートを生成させます。
- 成功レスポンス (200 OK):
{ "overall_assessment": { "is_publishable": true, "summary": "..." }, "completion_status": { "is_complete": true, "reasoning": "..." }, "prohibitions": [], "warnings": [], "dynamic_detailed_ratings": [] }ReviewReportスキーマに準拠したJSONオブジェクト。
- エラーレスポンス:
- Status Code:
400 Bad Request,500 Internal Server Error - Body:
{ "error": "Failed to review content", "details": "詳細なエラーメッセージ" }
- Status Code:
3.3. POST /api/ai/metadata-generate
作品のタイトル、あらすじ、タグなどのメタデータ案を、AIが原典言語で3つ生成します。
- 目的: 作者の母語でのメタデータ作成をAIが支援し、ユーザーの負担を軽減します。
- 認証: 必須。
- リクエストボディ (JSON):
{ "manuscripts": { ... }, // ProjectContent.manuscripts と同形式 "structuredPlot": { ... }, // StructuredPlot と同形式 "apiKey": "api-key", // フォールバック用APIキー "provider": "google", // (省略可) プロバイダーID "model": "gemini-2.5-flash", // (省略可) モデル名 "userApiKeys": { "google": "key" }, // (省略可) プロバイダー別APIキー "temperature": 0.7, // (省略可) "topP": 0.9 // (省略可) }manuscripts(必須): メタデータ生成の参考となる原稿データ。structuredPlot(必須): メタデータ生成の参考となるプロットデータ。apiKey(必須): フォールバック用APIキー。provider/model/userApiKeys(省略可): マルチプロバイダー対応。userApiKeys[provider]→ 環境変数 →apiKeyの順で解決。
- 処理フロー:
- 提供された原稿とプロットから、メタデータ生成のためのコンテキストを生成します。
- 指定されたプロバイダー/モデルでメタデータ提案を3つ生成させます。
- 成功レスポンス (200 OK):
{ "proposals": [ { "title": "提案タイトル1", "headline": "提案キャッチコピー1", "synopsis": "提案あらすじ1", "tags": ["タグ1", "タグ2"] }, // ... 他2つの提案 ] }MetadataGenerationResponseスキーマに準拠したJSONオブジェクト。
- エラーレスポンス:
- Status Code:
400 Bad Request,500 Internal Server Error - Body:
{ "error": "Failed to generate metadata", "details": "詳細なエラーメッセージ" }
- Status Code:
3.4. POST /api/ai/metadata-translate
確定した原典言語のメタデータを、AIが指定言語に翻訳します。
- 目的: グローバル公開のために、原典言語のメタデータを高品質な翻訳に変換します。
- 認証: 必須。
- リクエストボディ (JSON):
{ "metadata": { "title": "原典タイトル", "headline": "原典キャッチコピー", "synopsis": "原典あらすじ", "tags": ["原典タグ1", "原典タグ2"] }, "originalLanguage": "ja", // 原典言語コード (例: 'ja') "targetLanguage": "en", // (省略可、デフォルト: 'en') "apiKey": "api-key", // フォールバック用APIキー "provider": "google", // (省略可) プロバイダーID "model": "gemini-2.5-flash", // (省略可) モデル名 "userApiKeys": { "google": "key" }, // (省略可) プロバイダー別APIキー "temperature": 0.5, // (省略可) "topP": 0.8 // (省略可) }metadata(必須): 翻訳対象の原典言語メタデータ。originalLanguage(必須): 原典言語のBCP 47言語タグ(例:ja,en,zh-Hans)。targetLanguage(省略可): 翻訳先言語。デフォルトはen。apiKey(必須): フォールバック用APIキー。provider/model/userApiKeys(省略可): マルチプロバイダー対応。
- 処理フロー:
- 指定されたプロバイダー/モデルでメタデータを翻訳します。
- 成功レスポンス (200 OK):
{ "title": "Translated Title", "headline": "Translated Headline", "synopsis": "Translated synopsis...", "tags": ["Translated Tag1", "Translated Tag2"] }TranslatedMetadataスキーマに準拠したJSONオブジェクト。
- エラーレスポンス:
- Status Code:
400 Bad Request,500 Internal Server Error - Body:
{ "error": "Failed to translate metadata", "details": "詳細なエラーメッセージ" }
- Status Code:
3.5. POST /api/chat
汎用的なLLMとの対話を実現するエンドポイントです。ストリーミングと非ストリーミングの両方に対応しています。
-
目的: ユーザーからのメッセージ履歴を受け取り、選択されたLLMプロバイダーとモデルに応じた設定でAPIを呼び出し、AIの応答を返却します。
-
リクエストボディ (JSON):
{ "messages": [ { "role": "system", "content": "..." }, { "role": "user", "content": "..." }, { "role": "assistant", "content": "..." } ], "provider": "openai" | "google" | "anthropic" | "openrouter", "model": "gpt-4o", "userApiKeys": { "openai": "sk-...", "google": "..." }, "temperature": 0.7, "topP": 0.95, "stream": true }messages(必須): LangChain形式のメッセージ配列。provider(必須): 使用するLLMプロバイダーID。model(必須): 使用するモデル名。userApiKeys(任意): ユーザーが提供したAPIキーのオブジェクト。temperature(任意): 生成時の温度。デフォルトは0.7。topP(任意): トップPサンプリング。デフォルトは0.95。stream(任意): ストリーミング応答を要求するかどうか。デフォルトはtrue。
-
成功レスポンス:
- ストリーミング (
stream: true):- Content-Type:
text/plain; charset=utf-f - Body: AIからの応答テキストがチャンク形式で逐次送信される。
- Content-Type:
- 非ストリーミング (
stream: false):- Content-Type:
application/json - Body:
{ "content": "AIからの完全な応答テキスト" }
- Content-Type:
- ストリーミング (
-
エラーレスポンス:
- Status Code:
400 Bad Request,500 Internal Server Error - Body:
{ "error": "エラーメッセージ", "details": "詳細情報" }
- Status Code:
3.3. GET /api/models/openrouter
OpenRouter.aiで利用可能なLLMのモデルID一覧を取得します。
-
目的: クライアントサイドのモデル選択UIに、動的に最新のモデルリストを提供します。
-
リクエスト: なし
-
成功レスポンス:
- Content-Type:
application/json - Body:
{ "models": [ "openai/gpt-4o", "google/gemini-2.5-pro", "anthropic/claude-3.5-sonnet", ... ] }
- Content-Type:
-
エラーレスポンス:
- Status Code:
500 Internal Server Error - Body:
{ "error": "Failed to fetch models from OpenRouter.", "details": "詳細なエラーメッセージ" }
- Status Code:
3.4. POST /api/refine-plot-chunk
指定されたプロットの単一アイテム(Part, Arc, Chapterなど)を、ユーザーの指示に基づきAIが改善(リファイン)し、更新されたJSONオブジェクトを返します。
-
目的: ユーザーがプロットの特定の部分に集中し、AIと対話しながらその内容を洗練させることを可能にする。
-
リクエストボディ (JSON):
{ "targetItem": { ... }, "itemType": "part" | "arc" | "chapter" | "story" | "section", "context": { ... }, "instruction": "ユーザーからの具体的な指示テキスト...", "baseMessages": [ { "role": "system", "content": "..." } ], "provider": "google", "model": "gemini-2.5-pro", "userApiKeys": { ... }, "temperature": 0.7, "topP": 0.95 }targetItem(必須): リファイン対象となるプロットアイテムのJSONオブジェクト。itemType(必須):targetItemの階層を示す文字列。context(必須): 周辺の文脈を理解するための、プロジェクト全体のプロットオブジェクト。instruction(必須): AIに対する具体的な改善指示。baseMessages(必須): 執筆プロンプトなど、AIの振る舞いを規定するシステムメッセージ配列。provider,model,userApiKeys,temperature,topP:/api/chatと同様。
-
成功レスポンス:
- Content-Type:
application/json - Body:
itemTypeで指定されたスキーマに準拠した、更新後のプロットアイテムのJSONオブジェクト。{ "id": "...", // IDは変更されない "chapter_number": 1, "chapter_title": "(改善されたタイトル)", "theme": "(改善されたテーマ)", ... }
- Content-Type:
-
エラーレスポンス:
- Status Code:
400 Bad Request,500 Internal Server Error - Body:
{ "error": "Failed to refine plot chunk", "details": "詳細なエラーメッセージ" }
- Status Code:
3.5. POST /api/structure-plot
自然言語で記述されたプロットを、Zodスキーマに基づいた厳密な階層構造のJSONオブジェクトに変換します。
-
目的: ユーザーが自由形式で入力した小説のプロット案を、アプリケーションが解釈可能な形式的なデータ構造に変換し、執筆プロセスを支援します。
-
リクエストボディ (JSON):
{ "plotInput": "ユーザーが入力したプロットのテキスト...", "baseMessages": [ { "role": "system", "content": "..." } ], "provider": "google", "model": "gemini-2.5-pro", "userApiKeys": { ... }, "temperature": 0.5, "topP": 1.0, "plotConfig": { "parts": 1, "arcsPerPart": 2, "chaptersPerArc": 3, "storiesPerChapter": 2, "sectionsPerStory": 4, "wordsPerSection": 3000 }, "plotMetrics": { "totalStories": 12, "totalSections": 48, "totalWords": 144000, "wordsPerStory": 12000 } }plotInput(必須): 構造化対象のプロットテキスト。ユーザーは物語のコンセプト・設定のみを記述する。構成パラメータはplotConfig経由で別途注入される。baseMessages(必須): プロンプトカードなどから生成されたシステムプロンプトを含むメッセージ配列。provider,model,userApiKeys,temperature,topP:/api/chatと同様。plotConfig(任意): 構成パラメータ(部数、編数、章数、話数、節数、各節文字数)。指定時はplotPromptTemplate.tsのテンプレートを用いてユーザープロンプトが動的生成される。未指定時は従来のフォールバックプロンプトが使用される。plotMetrics(任意):plotConfigからplotConfigCalculator.tsで算出された指標(総話数、総文字数、1話あたり文字数など)。plotConfigとセットで使用される。
-
成功レスポンス:
-
Content-Type:
application/json -
Body:
types.tsで定義されたStructuredPlot型に準拠したJSONオブジェクト。{ "language": "ja", "parts": [ { "id": "a1b2c3d4-...", "part_number": 1, "part_title": "...", "arcs": [ { "id": "e5f6g7h8-...", "arc_number": 1, "arc_title": "...", "chapters": [ ... ] } ] } ] }UUID保証: 全階層(Part, Arc, Chapter, Story, Section)の
idフィールドには、アプリケーション側で機械的に生成された一意なUUID v4が設定されます。LLMの出力に依存しないため、一意性が保証されます。
-
-
エラーレスポンス:
- Status Code:
400 Bad Request,500 Internal Server Error - Body:
{ "error": "Failed to structure plot", "details": "詳細なエラーメッセージ" }
- Status Code:
4. 小説執筆生成の実装仕様(/api/chat の使用例)
小説のセクション執筆生成(generateSection)および翻訳(translateSection)は、専用のAPIエンドポイントではなく、汎用 /api/chat をプロンプトベースで使用しています。これにより、柔軟なプロンプト調整が可能ですが、仕様書への明記を怠ると実装の理解が難しくなります。
4.1. セクション生成 (generateSectionAtom の処理フロー)
- トリガー: LeftPanel.tsx 内の生成ボタン(Playアイコン)から
generateSectionAtomを呼び出し、addJobでジョブ化。 - API呼び出し:
POST /api/chat - 主なリクエストパラメータ(抜粋):
{ "messages": [ // writingPromptsAtom から baseMessages を構築 (執筆プロンプトカード) { "role": "system", "content": "執筆ガイドライン..." }, { "role": "user", "content": "# OVERALL PLOT STRUCTURE (全体プロットJSON)\n# 直近の文脈 (先行セクション)\n# 今回執筆する節 (章/話/節詳細, 目標文字数)\n# 言語生成モジュール規則 (読点控えめ, 繰り返し禁止)\n\n小説テキストのみ出力。" } ], "provider": "google|openai|anthropic|openrouter", "model": "gemini-2.5-pro|gpt-4o|...", "userApiKeys": { "google": "...", ... }, "temperature": 0.7, "topP": 0.95, "stream": true } - 文脈構築:
writingContextWindowAtomで制限した先行セクションのマヌスクリプト + 全体プロットJSON。 - レスポンス処理: ストリーミングで
manuscriptsAtomに更新、デバッグ情報記録 (debugInfo.prompt等)。
4.2. セクション翻訳 (translateSectionAtom)
- トリガー: Globeドロップダウンから
translateSectionAtom。 - プロンプト: 「プロの文芸翻訳家。直前文脈 + 原典テキスト を自然な[targetLang]に翻訳。テキストのみ出力。」
詳細実装: next-app/src/stores/aiGenerationAtoms.ts
将来的に専用API(/api/ai/generate-section)化を検討。
4.3. POST /api/tools/web-search
AIエージェントによるWeb検索機能を提供するエンドポイントです。Tavily Search APIをプロキシし、最新のWeb情報を取得します。
-
目的: エージェントモードのチャットで、AIが自律的にWeb検索を行えるようにします。
-
リクエストボディ (JSON):
{ "query": "検索クエリ文字列", "apiKey": "tvly-..." // ユーザーのTavily APIキー }query(必須): 検索クエリ。apiKey(必須): Tavily APIキー。
-
成功レスポンス (200 OK):
- Content-Type:
application/json - Body:
{ "results": [ { "title": "ページタイトル", "url": "https://example.com", "content": "ページ内容の要約...", "score": 0.95 }, ... ] }
- Content-Type:
-
エラーレスポンス:
- Status Code:
400 Bad Request,500 Internal Server Error - Body:
{ "error": "Failed to perform web search", "details": "詳細なエラーメッセージ" }
- Status Code:
詳細実装: next-app/src/app/api/tools/web-search/route.ts
関連ドキュメント:
12_ai_agent_architecture.md- エージェントアーキテクチャの詳細