fix(brain): comprehensive review fixes — placeholder persistence, count accuracy, dead code

- Gemini null 時: プレースホルダーを NULL で上書き(永続化バグ解消)
- /articles カウント: LIMIT後rows.filter()→専用COUNTクエリで正確化
- genAITogether 削除(genAI の alias で不要)
- quick-save: e.message のクライアント露出を固定メッセージに置換

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
posimai 2026-04-15 09:11:27 +09:00
parent 65ec560ebe
commit e960b9e2ac
1 changed files with 30 additions and 13 deletions

View File

@ -191,8 +191,6 @@ pool.on('error', (err) => {
const genAI = process.env.GEMINI_API_KEY const genAI = process.env.GEMINI_API_KEY
? new GoogleGenerativeAI(process.env.GEMINI_API_KEY) : null; ? new GoogleGenerativeAI(process.env.GEMINI_API_KEY) : null;
// Together 専用インスタンス(メインキーを共用)
const genAITogether = genAI;
// ── API Key 認証 ────────────────────────── // ── API Key 認証 ──────────────────────────
// API_KEYS="pk_maita_abc:maita,pk_partner_def:partner" // API_KEYS="pk_maita_abc:maita,pk_partner_def:partner"
@ -1389,14 +1387,20 @@ function buildRouter() {
sql += ' ORDER BY saved_at DESC LIMIT 300'; sql += ' ORDER BY saved_at DESC LIMIT 300';
try { 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 = { const counts = {
all: rows.length, all: countRows.reduce((s, r) => s + r.cnt, 0),
unread: rows.filter(a => a.status === 'inbox').length, unread: countMap['inbox'] || 0,
favorite: rows.filter(a => a.status === 'favorite').length, favorite: countMap['favorite'] || 0,
shared: rows.filter(a => a.status === 'shared').length shared: countMap['shared'] || 0,
}; };
res.json({ articles: rows, counts }); res.json({ articles: rows, counts });
@ -1465,7 +1469,14 @@ function buildRouter() {
if (checkRateLimit('gemini_analyze', savedUserId, 50, 60 * 60 * 1000)) { if (checkRateLimit('gemini_analyze', savedUserId, 50, 60 * 60 * 1000)) {
analyzeWithGemini(finalTitle, fullText || meta.desc, url).then(async (ai) => { 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(` await pool.query(`
UPDATE articles SET summary=$1, topics=$2, reading_time=$3 UPDATE articles SET summary=$1, topics=$2, reading_time=$3
WHERE user_id=$4 AND url=$5 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 AI Error]:', e));
} }
} catch (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)) { if (checkRateLimit('gemini_analyze', savedUserId, 50, 60 * 60 * 1000)) {
analyzeWithGemini(meta.title, fullText, url).then(async (ai) => { 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(` await pool.query(`
UPDATE articles SET summary=$1, topics=$2, reading_time=$3 UPDATE articles SET summary=$1, topics=$2, reading_time=$3
WHERE user_id=$4 AND url=$5 WHERE user_id=$4 AND url=$5
@ -1579,7 +1596,7 @@ function buildRouter() {
}); });
} catch (e) { } catch (e) {
if (!res.headersSent) res.status(500).send(`<h1>保存失敗: ${escapeHtml(e.message)}</h1>`); if (!res.headersSent) res.status(500).send(`<h1>保存に失敗しました</h1>`);
} }
}); });
@ -2379,7 +2396,7 @@ ${excerpt}
// 最初の ## 見出し以降を本文とみなし 4000 字を Gemini に渡す // 最初の ## 見出し以降を本文とみなし 4000 字を Gemini に渡す
const bodyStart = fullContent.search(/^#{1,2}\s/m); const bodyStart = fullContent.search(/^#{1,2}\s/m);
const excerpt = (bodyStart >= 0 ? fullContent.slice(bodyStart) : fullContent).slice(0, 4000); 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 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 timeoutP = new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 30000));
const result = await Promise.race([model.generateContent(prompt), timeoutP]); const result = await Promise.race([model.generateContent(prompt), timeoutP]);