From 1336b20c906339b3e5626dc7e25e21b37b7f6907 Mon Sep 17 00:00:00 2001 From: posimai Date: Thu, 9 Apr 2026 20:48:17 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20POST=20/save=20=E3=81=A8=20quick-save=20?= =?UTF-8?q?=E3=82=92=E5=8D=B3=E6=99=82=E4=BF=9D=E5=AD=98=E3=81=AB=E5=A4=89?= =?UTF-8?q?=E6=9B=B4=20=E2=80=94=20fetchMeta/Jina/AI=20=E3=82=92=E3=83=90?= =?UTF-8?q?=E3=83=83=E3=82=AF=E3=82=B0=E3=83=A9=E3=82=A6=E3=83=B3=E3=83=89?= =?UTF-8?q?=E3=81=B8=E7=A7=BB=E5=8B=95=E3=81=97=E3=81=A6=E3=83=A9=E3=82=B0?= =?UTF-8?q?=E8=A7=A3=E6=B6=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server.js | 161 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 90 insertions(+), 71 deletions(-) diff --git a/server.js b/server.js index ea1eed90..e0abcbff 100644 --- a/server.js +++ b/server.js @@ -1363,7 +1363,7 @@ function buildRouter() { } }); - // ========== 記事保存(Jina Reader自動取得対応)========== + // ========== 記事保存(即時保存 + バックグラウンドメタ取得)========== r.post('/save', authMiddleware, async (req, res) => { const { url, title: clientTitle, content, source: clientSource } = req.body || {}; if (!url) return res.status(400).json({ error: 'url is required' }); @@ -1373,56 +1373,59 @@ function buildRouter() { if (!['http:', 'https:'].includes(parsedUrl.protocol)) return res.status(400).json({ error: 'Only http/https' }); + const source = clientSource || extractSource(url); + const domain = parsedUrl.hostname; + try { - const meta = await fetchMeta(url); - let fullText = content || null; - const source = clientSource || extractSource(url); - - // 重要: contentが空の場合、Jina Reader APIで本文を自動取得 - if (!fullText || fullText.trim().length === 0) { - console.log(`[Brain API] No content provided for ${url}, attempting Jina Reader fetch...`); - - const jinaText = await fetchFullTextViaJina(url); - - if (jinaText && jinaText.length > 0) { - fullText = jinaText; - console.log(`[Brain API] ✓ Using Jina Reader full text (${fullText.length} chars)`); - } else { - // Jina Reader失敗時はOGP descriptionをフォールバック - console.log(`[Brain API] ⚠ Jina Reader failed, falling back to OGP description`); - fullText = meta.desc || ''; - } - } else { - console.log(`[Brain API] Using provided content (${fullText.length} chars)`); - } - - // 即座に保存してフロントに返す(AIはバックグラウンド) - let articleQuery = await pool.query(` - INSERT INTO articles (user_id, url, title, full_text, summary, topics, source, reading_time, favicon, og_image) - VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) - ON CONFLICT (user_id, url) DO UPDATE - SET title=EXCLUDED.title, full_text=EXCLUDED.full_text, source=EXCLUDED.source, summary='⏳ 再分析中...' - RETURNING * - `, [req.userId, url, clientTitle || meta.title, fullText, '⏳ AI分析中...', ['その他'], source, 3, meta.favicon, meta.ogImage]); - - let article = articleQuery.rows[0]; + // 1. URLだけ即座にDBへ保存してフロントに返す(メタ取得・AIはバックグラウンド) + const articleQuery = await pool.query(` + INSERT INTO articles (user_id, url, title, full_text, summary, topics, source, reading_time, favicon, og_image) + VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) + ON CONFLICT (user_id, url) DO UPDATE + SET source=EXCLUDED.source, summary='⏳ 再分析中...' + RETURNING * + `, [req.userId, url, clientTitle || domain, content || null, '⏳ AI分析中...', ['その他'], source, 3, + `https://www.google.com/s2/favicons?domain=${domain}&sz=32`, '']); + const article = articleQuery.rows[0]; res.json({ ok: true, article, aiStatus: 'pending' }); - // バックグラウンドでAI処理(ユーザーごとに 50記事/時間 まで) - if (checkRateLimit('gemini_analyze', req.userId, 50, 60 * 60 * 1000)) { - analyzeWithGemini(clientTitle || meta.title, fullText || meta.desc, url).then(async (ai) => { + // 2. バックグラウンドでメタ情報取得 → DB更新 → AI分析 + const savedUserId = req.userId; + setImmediate(async () => { + try { + const meta = await fetchMeta(url); + let fullText = content || null; + + if (!fullText || fullText.trim().length === 0) { + const jinaText = await fetchFullTextViaJina(url); + fullText = jinaText || meta.desc || ''; + } + + const finalTitle = clientTitle || meta.title; await pool.query(` - UPDATE articles SET summary=$1, topics=$2, reading_time=$3 - WHERE user_id=$4 AND url=$5 - `, [ai.summary, ai.topics, ai.readingTime, req.userId, url]); - console.log(`[Brain API] ✓ AI analysis completed for ${url}`); - }).catch(e => console.error('[Background AI Error]:', e)); - } + UPDATE articles SET title=$1, full_text=$2, favicon=$3, og_image=$4 + WHERE user_id=$5 AND url=$6 + `, [finalTitle, fullText, meta.favicon, meta.ogImage, savedUserId, url]); + + if (checkRateLimit('gemini_analyze', savedUserId, 50, 60 * 60 * 1000)) { + analyzeWithGemini(finalTitle, fullText || meta.desc, url).then(async (ai) => { + await pool.query(` + UPDATE articles SET summary=$1, topics=$2, reading_time=$3 + WHERE user_id=$4 AND url=$5 + `, [ai.summary, ai.topics, ai.readingTime, savedUserId, url]); + console.log(`[Brain API] ✓ AI analysis completed for ${url}`); + }).catch(e => console.error('[Background AI Error]:', e)); + } + } catch (e) { + console.error('[Background Meta Error]:', e.message); + } + }); } catch (e) { if (e.code === '23505') return res.status(409).json({ error: 'すでに保存済みです' }); - console.error(e); res.status(500).json({ error: 'DB error' }); + console.error(e); + if (!res.headersSent) res.status(500).json({ error: 'DB error' }); } }); @@ -1459,51 +1462,67 @@ function buildRouter() { } catch (e) { res.status(500).json({ error: 'DB error' }); } }); - // クイック保存 (Bookmarklet等からのGET) — Jina Reader対応 + // クイック保存 (Bookmarklet等からのGET) — 即時保存 + バックグラウンドメタ取得 r.get('/quick-save', authMiddleware, async (req, res) => { const url = req.query.url; if (!url) return res.status(400).send('

URL not provided

'); + let parsedUrl; + try { parsedUrl = new URL(url); } catch { return res.status(400).send('

Invalid URL

'); } + + const domain = parsedUrl.hostname; + const source = extractSource(url); + try { - const meta = await fetchMeta(url); - const source = extractSource(url); - - // Jina Readerで本文取得を試みる - let fullText = await fetchFullTextViaJina(url); - if (!fullText || fullText.length === 0) { - fullText = meta.desc || ''; - } - + // 1. URLだけ即座に保存 await pool.query(` INSERT INTO articles (user_id, url, title, full_text, summary, topics, source, reading_time, favicon, og_image) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) ON CONFLICT (user_id, url) DO UPDATE - SET title=EXCLUDED.title, full_text=EXCLUDED.full_text, source=EXCLUDED.source, summary='⏳ 再分析中...' - `, [req.userId, url, meta.title, fullText, '⏳ AI分析中...', ['その他'], source, 3, meta.favicon, meta.ogImage]); + SET source=EXCLUDED.source, summary='⏳ 再分析中...' + `, [req.userId, url, domain, null, '⏳ AI分析中...', ['その他'], source, 3, + `https://www.google.com/s2/favicons?domain=${domain}&sz=32`, '']); - // バックグラウンドAI(ユーザーごとに 50記事/時間 まで) - if (checkRateLimit('gemini_analyze', req.userId, 50, 60 * 60 * 1000)) { - analyzeWithGemini(meta.title, fullText, url).then(async (ai) => { - await pool.query(` - UPDATE articles SET summary=$1, topics=$2, reading_time=$3 - WHERE user_id=$4 AND url=$5 - `, [ai.summary, ai.topics, ai.readingTime, req.userId, url]); - }).catch(e => console.error('[Background AI Error]:', e)); - } - - // HTMLレスポンス(自動で閉じる) + // 2. HTMLレスポンスを即座に返す res.send(` 保存完了 -

✓ 保存しました

-

${escapeHtml(meta.title)}

-

AI分析をバックグラウンドで開始しました

- +

✓ 保存しました

+

${escapeHtml(domain)}

+

タイトル・AI分析をバックグラウンドで取得中...

+ `); + + // 3. バックグラウンドでメタ情報取得 → DB更新 → AI分析 + const savedUserId = req.userId; + setImmediate(async () => { + try { + const meta = await fetchMeta(url); + const jinaText = await fetchFullTextViaJina(url); + const fullText = jinaText || meta.desc || ''; + + await pool.query(` + UPDATE articles SET title=$1, full_text=$2, favicon=$3, og_image=$4 + WHERE user_id=$5 AND url=$6 + `, [meta.title, fullText, meta.favicon, meta.ogImage, savedUserId, url]); + + if (checkRateLimit('gemini_analyze', savedUserId, 50, 60 * 60 * 1000)) { + analyzeWithGemini(meta.title, fullText, url).then(async (ai) => { + await pool.query(` + UPDATE articles SET summary=$1, topics=$2, reading_time=$3 + WHERE user_id=$4 AND url=$5 + `, [ai.summary, ai.topics, ai.readingTime, savedUserId, url]); + }).catch(e => console.error('[Background AI Error]:', e)); + } + } catch (e) { + console.error('[Background Meta Error]:', e.message); + } + }); + } catch (e) { - res.status(500).send(`

保存失敗: ${escapeHtml(e.message)}

`); + if (!res.headersSent) res.status(500).send(`

保存失敗: ${escapeHtml(e.message)}

`); } });