# Synology 保存機能 — 構成とフロー図解 ## 全体構成図 ``` ┌─────────────────────────────────────────────────────────────────────┐ │ Posimai エコシステム │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌─────────────┐ │ │ │ Reader │ │ Feed │ │ Brain │ │ Maps │ │ │ │ (Vercel) │ │ (Vercel) │ │ (Vercel) │ │ (Vercel) │ │ │ │ │ │ │ │ │ │ │ │ │ │ 記事閲覧 │ │ RSS収集 │ │ 一覧表示 │ │ 位置情報 │ │ │ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ └─────────────┘ │ │ │ │ │ │ │ │ 「Brainに保存」ボタン │ │ │ └────────────────┴────────────────┘ │ │ │ │ │ │ POST /brain/api/save │ │ │ { url, title, content, source } │ │ ▼ │ │ ┌────────────────────────────────────────────┐ │ │ │ Synology NAS (Home Server) │ │ │ │ Tailscale: posimai-lab.tail72e846.ts.net │ │ │ ├────────────────────────────────────────────┤ │ │ │ ┌──────────────────────────────────────┐ │ │ │ │ │ Brain API (Node.js + Express) │ │ │ │ │ │ Port: 8090 │ │ │ │ │ │ Container: posimai_api │ │ │ │ │ │ │ │ │ │ │ │ 1. 認証(JWT/APIキー) │ │ │ │ │ │ 2. 本文処理(content || OGP取得) │ │ │ │ │ │ 3. Gemini AI 分析 ──────────────┐ │ │ │ │ │ │ 4. PostgreSQL保存 │ │ │ │ │ │ └───────────────────────────┬────────┘ │ │ │ │ │ │ │ │ │ │ │ ┌───────────────────────────▼──────────┐ │ │ │ │ │ PostgreSQL Database │ │ │ │ │ │ Port: 5432 │ │ │ │ │ │ Container: (別コンテナまたはSQLite) │ │ │ │ │ │ │ │ │ │ │ │ Table: articles │ │ │ │ │ │ ・id, user_id, url, title │ │ │ │ │ │ ・full_text (本文全文) ← Phase 1 │ │ │ │ │ │ ・summary (AI要約) │ │ │ │ │ │ ・topics (AIトピック抽出) │ │ │ │ │ │ ・reading_time (読了時間) │ │ │ │ │ │ ・favicon, og_image │ │ │ │ │ │ ・status (inbox/favorite/archived) │ │ │ │ │ └──────────────────────────────────────┘ │ │ │ │ │ │ │ │ ┌──────────────────────────────────────┐ │ │ │ │ │ Gemini API (External) │ │ │ │ │ │ Model: gemini-2.0-flash-exp │ │ │ │ │ │ │ │ │ │ │ │ Input: title + full_text (5000文字)│ │ │ │ │ │ Output: │ │ │ │ │ │ ・summary (3文の要約) │ │ │ │ │ │ ・topics (2つのテーマ) │ │ │ │ │ │ ・readingTime (推定分数) │ │ │ │ │ └──────────────────────────────────────┘ │ │ │ └────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────┘ ``` --- ## 保存フロー詳細(シーケンス図) ### 1. Reader/Feed → Brain 保存フロー ``` ┌─────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────┐ │ Reader │ │ Synology │ │ PostgreSQL │ │ Gemini │ │ (Vercel)│ │ Brain API │ │ Database │ │ API │ └────┬────┘ └──────┬───────┘ └──────┬───────┘ └────┬─────┘ │ │ │ │ │ 1. ユーザーが │ │ │ │ 「Brainに保存」 │ │ │ │ ボタンをクリック │ │ │ │ │ │ │ │ 2. POST /save │ │ │ │ { │ │ │ │ url: "...", │ │ │ │ title: "...", │ │ │ │ content: "...", │ (本文全文) │ │ │ source: "reader"│ │ │ │ } │ │ │ ├───────────────────>│ │ │ │ │ │ │ │ │ 3. 認証チェック │ │ │ │ (JWT/APIキー) │ │ │ │ │ │ │ │ 4. 本文取得 │ │ │ │ ・content あり │ │ │ │ → そのまま使用 │ │ │ │ ・content なし │ │ │ │ → fetchOGP() │ │ │ │ (OGP description │ │ │ │ を代替使用) │ │ │ │ │ │ │ │ 5. AI分析リクエスト │ │ │ │ analyzeWithGemini( │ │ │ │ title, │ │ │ │ fullText, │ │ │ │ url │ │ │ │ ) │ │ │ ├───────────────────────────────────────────>│ │ │ │ │ │ │ │ 6. AI処理 │ │ │ │ ・要約生成 │ │ │ │ ・トピック抽出 │ │ │ │ ・読了時間推定 │ │ │ │ (5000文字制限) │ │ │ │ │ │ │ 7. AI分析結果 │ │ │ │ { │ │ │ │ summary: "...", │ │ │ │ topics: [...], │ │ │ │ readingTime: 5 │ │ │ │ } │ │ │ │<──────────────────────────────────────────┤ │ │ │ │ │ │ 8. DB保存 │ │ │ │ INSERT INTO articles │ │ │ │ (user_id, url, │ │ │ │ title, full_text, │ ← Phase 1 新規 │ │ │ summary, topics, │ │ │ │ source, ...) │ │ │ │ ON CONFLICT UPDATE │ (重複時は更新) │ │ ├──────────────────────>│ │ │ │ │ │ │ │ 9. 保存完了 │ │ │ │<──────────────────────┤ │ │ │ │ │ │ 10. レスポンス │ │ │ │ { │ │ │ │ success: true, │ │ │ │ articleId: 123, │ │ │ │ fullTextSaved: │ │ │ │ true, │ │ │ │ textLength: 2500│ │ │ │ } │ │ │ │<───────────────────┤ │ │ │ │ │ │ │ 11. UI フィードバック │ │ │ ・ボタンを緑色に │ │ │ │ ・「保存済み」表示 │ │ │ │ │ │ │ ``` --- ### 2. Brain 一覧表示フロー ``` ┌─────────┐ ┌──────────────┐ ┌──────────────┐ │ Brain │ │ Synology │ │ PostgreSQL │ │ (Vercel)│ │ Brain API │ │ Database │ └────┬────┘ └──────┬───────┘ └──────┬───────┘ │ │ │ │ 1. ページを開く │ │ │ (初回ロード) │ │ │ │ │ │ 2. GET /articles │ │ │ Authorization: │ │ │ Bearer pk_xxx │ │ ├───────────────────>│ │ │ │ │ │ │ 3. 認証チェック │ │ │ │ │ │ 4. 記事一覧取得 │ │ │ SELECT id, url, │ │ │ title, full_text, │ ← Phase 1 追加 │ │ summary, topics, │ │ │ ... │ │ │ FROM articles │ │ │ WHERE user_id = $1 │ │ │ ORDER BY │ │ │ created_at DESC │ │ ├──────────────────────>│ │ │ │ │ │ 5. 記事データ │ │ │<──────────────────────┤ │ │ │ │ 6. JSON レスポンス│ │ │ { │ │ │ articles: [ │ │ │ { │ │ │ id: 123, │ │ │ url: "...", │ │ │ title: "...│ │ │ fullText: │ ← Phase 1 新規 │ │ "...", │ (本文表示用) │ │ summary: │ │ │ "...", │ │ │ topics: [...│ │ │ } │ │ │ ] │ │ │ } │ │ │<───────────────────┤ │ │ │ │ │ 7. UI レンダリング│ │ │ ・カード一覧表示 │ │ │ ・クリックで詳細 │ │ │ モーダル表示 │ │ │ │ │ ``` --- ### 3. Brain 本文表示フロー(詳細モーダル) ``` ┌─────────┐ ┌──────────────┐ │ Brain │ │ Memory │ │ UI │ │ Cache │ └────┬────┘ └──────┬───────┘ │ │ │ 1. カードをクリック│ │ openArticleDetail │ │ Modal(article) │ │ │ │ 2. キャッシュ確認 │ │ fullTextCache │ │ .has(article.id)│ ├───────────────────>│ │ │ │ 3a. キャッシュHIT │ │ (2回目以降) │ │<───────────────────┤ │ fullText を即表示 │ │ (APIコールなし) │ │ │ │ 3b. キャッシュMISS│ │ (初回) │ │ │ │ 4. article.fullText│ │ がすでに含まれて │ │ いる場合 │ │ → 直接表示 │ │ (Phase 1で対応済み)│ │ │ │ 5. Markdown変換 │ │ renderMarkdown( │ │ fullText │ │ ) │ │ ・# →

│ │ ・**text** → │ │ │ │ ・[link](url) → │ │ │ │ │ │ 6. XSS対策 │ │ DOMPurify │ │ .sanitize() │ │ │ │ 7. 表示 │ │ モーダル内に │ │ HTML レンダリング │ │ (CSS: .article- │ │ detail-md) │ │ │ │ 8. キャッシュ保存 │ │ fullTextCache │ │ .set(id, text) │ ├───────────────────>│ │ │ ``` --- ## データの流れ(Phase 1: 本文全文保存) ### Before Phase 1(従来) ``` Reader/Feed │ ├─ URL + Title のみ送信 │ ▼ Synology API │ ├─ fetchOGP() で OGP description 取得(短い) │ ├─ Gemini 分析(短い説明のみ) │ → 要約の質が低い │ ▼ PostgreSQL │ └─ summary (AI要約、精度低) └─ full_text: NULL ← 本文なし! Brain UI │ └─ 「本文を読む」ボタンなし └─ AI要約のみ表示(精度低) ``` ### After Phase 1(現在) ``` Reader/Feed │ ├─ URL + Title + Content (本文全文) を送信 │ ▼ Synology API │ ├─ content あり → そのまま使用 │ content なし → fetchOGP() で代替 │ ├─ Gemini 分析(本文全文から5000文字) │ → 高精度な要約・トピック抽出 │ ▼ PostgreSQL │ ├─ full_text: "記事の本文全文..." ← Phase 1 新規! ├─ summary: "高精度AI要約" └─ topics: ["技術", "Web開発"] Brain UI │ ├─ 一覧: AI要約を表示 │ └─ クリック → モーダル │ ├─ 「本文を読む」タブ │ → full_text を Markdown 表示 │ └─ メモリキャッシュ(2回目以降は即表示) ``` --- ## 技術スタック | レイヤー | 技術 | 用途 | |---------|------|------| | **Frontend** | HTML/CSS/JS (Vanilla) | Brain UI (Vercel) | | **API Server** | Node.js + Express | Synology NAS (Docker) | | **Database** | PostgreSQL | Synology NAS (Docker) | | **AI** | Gemini 2.0 Flash Exp | 要約・トピック抽出 | | **Network** | Tailscale | Synology へのセキュアアクセス | | **Hosting** | Vercel (Frontend) + Synology (Backend) | ハイブリッド構成 | --- ## Phase 1 の主な変更点 ### 1. データベース **Migration**: [synology-brain-migration.sql](synology-brain-migration.sql) ```sql -- 本文全文カラムを追加 ALTER TABLE articles ADD COLUMN IF NOT EXISTS full_text TEXT; -- 画像用(将来用) ALTER TABLE articles ADD COLUMN IF NOT EXISTS images TEXT[]; -- 更新日時用 ALTER TABLE articles ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP DEFAULT NOW(); ``` ### 2. API エンドポイント **File**: [synology-brain-api-save-endpoint.js](synology-brain-api-save-endpoint.js) #### POST /save の変更点 - **Before**: `{ url, title, source }` のみ - **After**: `{ url, title, content, source }` ← `content` 追加 ```javascript // Before: OGP description のみ const meta = await fetchOGP(url); const ai = await analyzeWithGemini(title, meta.desc, url); // ^^^^^^^^ 短い // After: 本文全文を優先 let fullText = content || null; if (!fullText) { meta = await fetchOGP(url); fullText = meta.desc || ''; // フォールバック } const ai = await analyzeWithGemini(title, fullText, url); // ^^^^^^^^^ 長い! ``` #### GET /articles の変更点 - **Before**: `full_text` カラムを返さない - **After**: `full_text` を含めて返す ```javascript // Before SELECT id, url, title, summary, topics, ... FROM articles // After SELECT id, url, title, full_text, summary, topics, ... // ^^^^^^^^^ 追加 FROM articles ``` ### 3. Brain UI (Vercel) **File**: [posimai-brain/js/ui/actions.js](posimai-brain/js/ui/actions.js) #### Markdown レンダリング ```javascript // 1. Markdown → HTML 変換 function renderMarkdown(md) { // # →

, **text** → , [link](url) → // ... } // 2. XSS 対策(DOMPurify) DOMPurify.sanitize(html, { ALLOWED_TAGS: ['p', 'h1', 'h2', 'h3', 'strong', 'em', 'code', 'a', 'hr'] }); ``` #### メモリキャッシュ ```javascript const fullTextCache = new Map(); // 初回: API から取得 → キャッシュ fullTextCache.set(article.id, fullText); // 2回目以降: キャッシュから即表示 if (fullTextCache.has(article.id)) { return cachedText; // APIコールなし! } ``` **File**: [posimai-brain/style.css](posimai-brain/style.css) #### Markdown CSS ```css .article-detail-md h1 { font-size: 24px; ... } .article-detail-md h2 { font-size: 20px; ... } .article-detail-md a { color: var(--accent); ... } .article-detail-md code { background: var(--surface2); ... } ``` --- ## セキュリティ考慮 | 対策 | 実装内容 | 効果 | |-----|---------|------| | **認証** | JWT/APIキー (authMiddleware) | 未認証アクセス防止 | | **XSS対策** | DOMPurify でサニタイズ | スクリプト埋め込み防止 | | **SQL Injection** | パラメータ化クエリ (`$1`, `$2`, ...) | SQL攻撃防止 | | **HTTPS** | Vercel (自動), Tailscale (暗号化) | 通信盗聴防止 | | **CORS** | 設定済み (Brain API) | 不正ドメインからのアクセス防止 | --- ## パフォーマンス最適化 | 項目 | 実装 | 効果 | |-----|------|------| | **AI トークン制限** | 本文 5000 文字まで | コスト削減 | | **メモリキャッシュ** | Map-based cache | 2回目以降の表示が即座 | | **ON CONFLICT** | PostgreSQL UPSERT | 重複チェック不要 | | **インデックス** | `user_id`, `created_at` | クエリ高速化 | --- ## トラブルシューティング ### 問題 1: 本文が保存されない **確認事項**: 1. Reader/Feed が `content` を送信しているか? - DevTools → Network → `/save` リクエストの `body` を確認 2. Synology API が `full_text` カラムに保存しているか? - PostgreSQL: `SELECT full_text FROM articles ORDER BY created_at DESC LIMIT 1;` 3. マイグレーションは実行済みか? - `\d articles` で `full_text` カラムがあるか確認 ### 問題 2: Markdown が表示されない **確認事項**: 1. DOMPurify が読み込まれているか? - DevTools → Console: `window.DOMPurify` が存在するか 2. CSS が適用されているか? - DevTools → Elements: `.article-detail-md` クラスがあるか 3. Markdown 変換が動作しているか? - DevTools → Console: `renderMarkdown("# Test")` を実行 ### 問題 3: AI 分析が失敗する **確認事項**: 1. Gemini API キーは正しいか? - Synology: 環境変数 `GEMINI_API_KEY` を確認 2. 本文が長すぎないか? - 5000 文字以上は自動でカットされる 3. ログを確認 - `docker logs -f posimai-brain-api` --- ## まとめ Phase 1 の「本文全文保存」機能により: ✅ **データロスなし**: Reader で読んだ記事の本文が完全保存される ✅ **高精度AI分析**: 本文全文から要約・トピック抽出 ✅ **オフライン閲覧可**: Brain に保存した記事は元サイトが消えても読める ✅ **UX向上**: メモリキャッシュで2回目以降の表示が高速 ✅ **セキュア**: XSS対策、認証、HTTPS完備 次のステップ: Synology 側の実装完了を確認し、エンドツーエンドテストを実施。