From 0d509461acdac75e951ec82787c4383fdcf1adb1 Mon Sep 17 00:00:00 2001 From: posimai Date: Wed, 22 Apr 2026 09:02:11 +0900 Subject: [PATCH] =?UTF-8?q?fix(together):=20legacy=20path=20=E5=BB=83?= =?UTF-8?q?=E6=AD=A2(JWT=E5=BF=85=E9=A0=88=E5=8C=96)=E3=83=BBGemini=202.5-?= =?UTF-8?q?flash=20503=20=E3=83=95=E3=82=A9=E3=83=BC=E3=83=AB=E3=83=90?= =?UTF-8?q?=E3=83=83=E3=82=AF=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server.js | 122 ++++++++++++++++++++++++++---------------------------- 1 file changed, 58 insertions(+), 64 deletions(-) diff --git a/server.js b/server.js index 00e94546..48709d7e 100644 --- a/server.js +++ b/server.js @@ -807,42 +807,24 @@ async function togetherEnsureMember(pool, res, groupId, username, jwtUserId) { return false; } try { - if (jwtUserId) { - const strict = await pool.query( - `SELECT 1 FROM together_members m - WHERE m.group_id = $1 AND ( - m.user_id = $2 - OR ( - (m.user_id IS NULL OR btrim(COALESCE(m.user_id, '')) = '') - AND m.username = ANY($3::text[]) - ) - )`, - [gidNum, jwtUserId, usernames] - ); - if (strict.rows.length > 0) return true; - - const legacy = await pool.query( - 'SELECT 1 FROM together_members WHERE group_id=$1 AND username = ANY($2::text[])', - [gidNum, usernames] - ); - if (legacy.rows.length > 0) { - // user_id 未紐付け期間の暫定: メンバー行があれば許可(紐付け完了後に削除予定) - console.warn('[Together] legacy path used user=%s usernames=%j group=%s', jwtUserId, usernames, gidNum); - return true; - } - res.status(403).json({ error: 'グループのメンバーではありません' }); + if (!jwtUserId) { + res.status(401).json({ error: '認証が必要です' }); return false; } - const primaryUsername = usernames[0]; - const legacyOnly = await pool.query( - 'SELECT 1 FROM together_members WHERE group_id=$1 AND username=$2', - [gidNum, primaryUsername] + const strict = await pool.query( + `SELECT 1 FROM together_members m + WHERE m.group_id = $1 AND ( + m.user_id = $2 + OR ( + (m.user_id IS NULL OR btrim(COALESCE(m.user_id, '')) = '') + AND m.username = ANY($3::text[]) + ) + )`, + [gidNum, jwtUserId, usernames] ); - if (legacyOnly.rows.length === 0) { - res.status(403).json({ error: 'グループのメンバーではありません' }); - return false; - } - return true; + if (strict.rows.length > 0) return true; + res.status(403).json({ error: 'グループのメンバーではありません' }); + return false; } catch (e) { console.error('[Together] togetherEnsureMember', e.message); res.status(500).json({ error: 'Internal server error' }); @@ -2629,25 +2611,30 @@ ${excerpt} let summary = null; let tags = []; if (togetherGenAI && fullContent) { - try { - // 最初の ## 見出し以降を本文とみなし 4000 字を Gemini に渡す - const bodyStart = fullContent.search(/^#{1,2}\s/m); - const excerpt = (bodyStart >= 0 ? fullContent.slice(bodyStart) : fullContent).slice(0, 4000); - const model = togetherGenAI.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]); - const raw = result.response.text().trim(); + const bodyStart = fullContent.search(/^#{1,2}\s/m); + const excerpt = (bodyStart >= 0 ? fullContent.slice(bodyStart) : fullContent).slice(0, 4000); + 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 modelsToTry = ['gemini-2.5-flash', 'gemini-2.0-flash']; + for (const modelName of modelsToTry) { try { - const parsed = JSON.parse(raw); - summary = (parsed.summary || '').slice(0, 300); - tags = Array.isArray(parsed.tags) ? parsed.tags.slice(0, 4).map(t => String(t).slice(0, 20)) : []; - } catch { - summary = raw.slice(0, 300); + const model = togetherGenAI.getGenerativeModel({ model: modelName }); + const timeoutP = new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 30000)); + const result = await Promise.race([model.generateContent(prompt), timeoutP]); + const raw = result.response.text().trim(); + try { + const parsed = JSON.parse(raw); + summary = (parsed.summary || '').slice(0, 300); + tags = Array.isArray(parsed.tags) ? parsed.tags.slice(0, 4).map(t => String(t).slice(0, 20)) : []; + } catch { + summary = raw.slice(0, 300); + } + break; + } catch (aiErr) { + console.error(`[together archive AI] ${modelName} share=${shareId}`, aiErr.message); + if (modelName === modelsToTry[modelsToTry.length - 1]) { + // 全モデル失敗: Jina 本文は保存済みなので Reader は使える状態で done にする + } } - } catch (aiErr) { - // Gemini 失敗(503等): Jina 本文は保存済みなので Reader は使える状態で done にする - console.error('[together archive AI]', shareId, aiErr.message); } } @@ -2683,20 +2670,27 @@ ${excerpt} if (!togetherGenAI) return res.status(503).json({ error: 'AI が設定されていません' }); await pool.query(`UPDATE together_shares SET archive_status='pending' WHERE id=$1`, [shareId]); res.json({ ok: true, status: 'pending' }); - try { - const bodyStart = share.full_content.search(/^#{1,2}\s/m); - const excerpt = (bodyStart >= 0 ? share.full_content.slice(bodyStart) : share.full_content).slice(0, 4000); - const model = togetherGenAI.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個。「その他」は絶対に使わないこと\n\n記事:\n${excerpt}`; - const timeoutP = new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 30000)); - const result = await Promise.race([model.generateContent(prompt), timeoutP]); - const raw = result.response.text().trim(); - let summary = null, tags = []; - try { const p = JSON.parse(raw); summary = (p.summary || '').slice(0, 300); tags = Array.isArray(p.tags) ? p.tags.slice(0, 4).map(t => String(t).slice(0, 20)) : []; } - catch { summary = raw.slice(0, 300); } - await pool.query(`UPDATE together_shares SET summary=$1, tags=$2, archive_status='done' WHERE id=$3`, [summary, tags, shareId]); - } catch (e) { - console.error('[rearchive gemini]', shareId, e.message); + const bodyStart = share.full_content.search(/^#{1,2}\s/m); + const excerpt = (bodyStart >= 0 ? share.full_content.slice(bodyStart) : share.full_content).slice(0, 4000); + const prompt = `以下の記事を分析して、JSONのみを返してください(コードブロック不要)。\n\n{"summary":"1〜2文の日本語要約","tags":["タグ1","タグ2","タグ3"]}\n\n- summary: 読者が読む価値があるかを判断できる1〜2文\n- tags: 内容を表す具体的な日本語タグを2〜4個。「その他」は絶対に使わないこと\n\n記事:\n${excerpt}`; + let rearchiveDone = false; + for (const modelName of ['gemini-2.5-flash', 'gemini-2.0-flash']) { + try { + const model = togetherGenAI.getGenerativeModel({ model: modelName }); + const timeoutP = new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 30000)); + const result = await Promise.race([model.generateContent(prompt), timeoutP]); + const raw = result.response.text().trim(); + let summary = null, tags = []; + try { const p = JSON.parse(raw); summary = (p.summary || '').slice(0, 300); tags = Array.isArray(p.tags) ? p.tags.slice(0, 4).map(t => String(t).slice(0, 20)) : []; } + catch { summary = raw.slice(0, 300); } + await pool.query(`UPDATE together_shares SET summary=$1, tags=$2, archive_status='done' WHERE id=$3`, [summary, tags, shareId]); + rearchiveDone = true; + break; + } catch (e) { + console.error(`[rearchive ${modelName}]`, shareId, e.message); + } + } + if (!rearchiveDone) { await pool.query(`UPDATE together_shares SET archive_status='done' WHERE id=$1`, [shareId]); } } else {