26 KiB
26 KiB
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 │
│ ) │
│ ・# → <h1> │
│ ・**text** → │
│ <strong> │
│ ・[link](url) → │
│ <a href> │
│ │
│ 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
-- 本文全文カラムを追加
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
POST /save の変更点
- Before:
{ url, title, source }のみ - After:
{ url, title, content, source }←content追加
// 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を含めて返す
// 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
Markdown レンダリング
// 1. Markdown → HTML 変換
function renderMarkdown(md) {
// # → <h1>, **text** → <strong>, [link](url) → <a>
// ...
}
// 2. XSS 対策(DOMPurify)
DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['p', 'h1', 'h2', 'h3', 'strong', 'em', 'code', 'a', 'hr']
});
メモリキャッシュ
const fullTextCache = new Map();
// 初回: API から取得 → キャッシュ
fullTextCache.set(article.id, fullText);
// 2回目以降: キャッシュから即表示
if (fullTextCache.has(article.id)) {
return cachedText; // APIコールなし!
}
File: posimai-brain/style.css
Markdown 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: 本文が保存されない
確認事項:
- Reader/Feed が
contentを送信しているか?- DevTools → Network →
/saveリクエストのbodyを確認
- DevTools → Network →
- Synology API が
full_textカラムに保存しているか?- PostgreSQL:
SELECT full_text FROM articles ORDER BY created_at DESC LIMIT 1;
- PostgreSQL:
- マイグレーションは実行済みか?
\d articlesでfull_textカラムがあるか確認
問題 2: Markdown が表示されない
確認事項:
- DOMPurify が読み込まれているか?
- DevTools → Console:
window.DOMPurifyが存在するか
- DevTools → Console:
- CSS が適用されているか?
- DevTools → Elements:
.article-detail-mdクラスがあるか
- DevTools → Elements:
- Markdown 変換が動作しているか?
- DevTools → Console:
renderMarkdown("# Test")を実行
- DevTools → Console:
問題 3: AI 分析が失敗する
確認事項:
- Gemini API キーは正しいか?
- Synology: 環境変数
GEMINI_API_KEYを確認
- Synology: 環境変数
- 本文が長すぎないか?
- 5000 文字以上は自動でカットされる
- ログを確認
docker logs -f posimai-brain-api
まとめ
Phase 1 の「本文全文保存」機能により:
✅ データロスなし: Reader で読んだ記事の本文が完全保存される ✅ 高精度AI分析: 本文全文から要約・トピック抽出 ✅ オフライン閲覧可: Brain に保存した記事は元サイトが消えても読める ✅ UX向上: メモリキャッシュで2回目以降の表示が高速 ✅ セキュア: XSS対策、認証、HTTPS完備
次のステップ: Synology 側の実装完了を確認し、エンドツーエンドテストを実施。