From e960b9e2ac2f93870b927bfec798b4b82b1b0eec Mon Sep 17 00:00:00 2001 From: posimai Date: Wed, 15 Apr 2026 09:11:27 +0900 Subject: [PATCH] =?UTF-8?q?fix(brain):=20comprehensive=20review=20fixes=20?= =?UTF-8?q?=E2=80=94=20placeholder=20persistence,=20count=20accuracy,=20de?= =?UTF-8?q?ad=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Gemini null 時: ⏳プレースホルダーを NULL で上書き(永続化バグ解消) - /articles カウント: LIMIT後rows.filter()→専用COUNTクエリで正確化 - genAITogether 削除(genAI の alias で不要) - quick-save: e.message のクライアント露出を固定メッセージに置換 Co-Authored-By: Claude Sonnet 4.6 --- server.js | 43 ++++++++++++++++++++++++++++++------------- 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/server.js b/server.js index 0b6b4bbc..6fdcdf38 100644 --- a/server.js +++ b/server.js @@ -191,8 +191,6 @@ pool.on('error', (err) => { const genAI = process.env.GEMINI_API_KEY ? new GoogleGenerativeAI(process.env.GEMINI_API_KEY) : null; -// Together 専用インスタンス(メインキーを共用) -const genAITogether = genAI; // ── API Key 認証 ────────────────────────── // API_KEYS="pk_maita_abc:maita,pk_partner_def:partner" @@ -1389,14 +1387,20 @@ function buildRouter() { sql += ' ORDER BY saved_at DESC LIMIT 300'; try { - const { rows } = await pool.query(sql, params); + const [{ rows }, { rows: countRows }] = await Promise.all([ + pool.query(sql, params), + pool.query( + `SELECT status, COUNT(*)::int AS cnt FROM articles WHERE user_id=$1 GROUP BY status`, + [req.userId] + ) + ]); - // カウント計算 + const countMap = Object.fromEntries(countRows.map(r => [r.status, r.cnt])); const counts = { - all: rows.length, - unread: rows.filter(a => a.status === 'inbox').length, - favorite: rows.filter(a => a.status === 'favorite').length, - shared: rows.filter(a => a.status === 'shared').length + all: countRows.reduce((s, r) => s + r.cnt, 0), + unread: countMap['inbox'] || 0, + favorite: countMap['favorite'] || 0, + shared: countMap['shared'] || 0, }; res.json({ articles: rows, counts }); @@ -1465,7 +1469,14 @@ function buildRouter() { if (checkRateLimit('gemini_analyze', savedUserId, 50, 60 * 60 * 1000)) { analyzeWithGemini(finalTitle, fullText || meta.desc, url).then(async (ai) => { - if (!ai) { console.warn(`[Brain API] AI analysis skipped (null) for ${url}`); return; } + if (!ai) { + console.warn(`[Brain API] AI analysis failed for ${url}, clearing placeholder`); + await pool.query( + `UPDATE articles SET summary=NULL WHERE user_id=$1 AND url=$2 AND summary LIKE '⏳%'`, + [savedUserId, url] + ); + return; + } await pool.query(` UPDATE articles SET summary=$1, topics=$2, reading_time=$3 WHERE user_id=$4 AND url=$5 @@ -1474,7 +1485,7 @@ function buildRouter() { }).catch(e => console.error('[Background AI Error]:', e)); } } catch (e) { - console.error('[Background Meta Error]:', e.message); + console.error('[Background Meta Error]:', e.message || e); } }); @@ -1566,7 +1577,13 @@ function buildRouter() { if (checkRateLimit('gemini_analyze', savedUserId, 50, 60 * 60 * 1000)) { analyzeWithGemini(meta.title, fullText, url).then(async (ai) => { - if (!ai) { console.warn(`[Brain API] AI analysis skipped (null) for ${url}`); return; } + if (!ai) { + await pool.query( + `UPDATE articles SET summary=NULL WHERE user_id=$1 AND url=$2 AND summary LIKE '⏳%'`, + [savedUserId, url] + ); + return; + } await pool.query(` UPDATE articles SET summary=$1, topics=$2, reading_time=$3 WHERE user_id=$4 AND url=$5 @@ -1579,7 +1596,7 @@ function buildRouter() { }); } catch (e) { - if (!res.headersSent) res.status(500).send(`

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

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

保存に失敗しました

`); } }); @@ -2379,7 +2396,7 @@ ${excerpt} // 最初の ## 見出し以降を本文とみなし 4000 字を Gemini に渡す const bodyStart = fullContent.search(/^#{1,2}\s/m); const excerpt = (bodyStart >= 0 ? fullContent.slice(bodyStart) : fullContent).slice(0, 4000); - const model = genAITogether.getGenerativeModel({ model: 'gemini-2.5-flash' }); + const model = genAI.getGenerativeModel({ model: 'gemini-2.5-flash' }); const prompt = `以下の記事を分析して、JSONのみを返してください(コードブロック不要)。\n\n{"summary":"1〜2文の日本語要約","tags":["タグ1","タグ2","タグ3"]}\n\n- summary: 読者が読む価値があるかを判断できる1〜2文\n- tags: 内容を表す具体的な日本語タグを2〜4個。「その他」は絶対に使わないこと。内容が不明な場合でも最も近いカテゴリを選ぶ(例: AI, テクノロジー, ビジネス, 健康, 旅行, 料理, スポーツ, 政治, 経済, エンタメ, ゲーム, 科学, デザイン, ライフスタイル, 教育, 環境, 医療, 法律, 文化, 歴史)\n\n記事:\n${excerpt}`; const timeoutP = new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 30000)); const result = await Promise.race([model.generateContent(prompt), timeoutP]);