posimai-root/docs/synology/SYNOLOGY_SAVE_FLOW_DIAGRAM.md

526 lines
26 KiB
Markdown
Raw Normal View History

# 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](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) {
// # → <h1>, **text**<strong>, [link](url) → <a>
// ...
}
// 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 側の実装完了を確認し、エンドツーエンドテストを実施。