fix(together): legacy path 廃止(JWT必須化)・Gemini 2.5-flash 503 フォールバック追加

This commit is contained in:
posimai 2026-04-22 09:02:11 +09:00
parent b00e31cb90
commit 0d509461ac
1 changed files with 58 additions and 64 deletions

122
server.js
View File

@ -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 {