Compare commits
2 Commits
b00e31cb90
...
45893eb453
| Author | SHA1 | Date |
|---|---|---|
|
|
45893eb453 | |
|
|
0d509461ac |
12
STATUS.md
12
STATUS.md
|
|
@ -10,14 +10,14 @@
|
||||||
|
|
||||||
- **posimai-boki** は独立リポジトリで Gitea/GitHub へ push 済み。追加分を出したときは `cd posimai-boki && npm run deploy`
|
- **posimai-boki** は独立リポジトリで Gitea/GitHub へ push 済み。追加分を出したときは `cd posimai-boki && npm run deploy`
|
||||||
- **article-keeper フォルダ削除**(エクスプローラーから手動削除): Firebase プロジェクトは削除済み。キーは git 未追跡なので履歴汚染なし。フォルダを消すだけで OK。
|
- **article-keeper フォルダ削除**(エクスプローラーから手動削除): Firebase プロジェクトは削除済み。キーは git 未追跡なので履歴汚染なし。フォルダを消すだけで OK。
|
||||||
- **contact.html の Formspree ID 設定**: formspree.io でフォームを作成し `YOUR_FORM_ID` を差し替え → 実送信テスト → Store 本番化ブロッカー解消。
|
- **contact.html**: Resend API 経由に切り替え済み・送受信テスト完了(2026-04-21)✓
|
||||||
|
- **Stripe 本番化時**: index.html・index-b.html・index-c.html・index-d.html の 4ファイルにある `test_9B67sEbN3fowfMW4jwenS00` を本番 Payment Link URL に一括差し替え(Cursor 指摘 2026-04-22)
|
||||||
|
|
||||||
## Together legacy path 廃止計画(2026-04-21)
|
## Together legacy path 廃止(2026-04-22 完了)
|
||||||
|
|
||||||
- **現状**: `jwtUserId` なし(`?u=` のみ)でメンバー照合が通る経路を許容中(warn ログあり)
|
- Docker ログで legacy hit 0件を確認の上、JWT 必須化済み(server.js `togetherEnsureMember`)
|
||||||
- **invite_code 漏洩は修正済み**: JWT なしリクエストでは `invite_code` をレスポンスから除外(2026-04-21 パッチ適用)
|
- JWT なしリクエストは 401 を返す
|
||||||
- **legacy path 廃止期限**: 2026-04-28(今週末)を目安に `togetherEnsureMember` の legacy 経路を削除し、JWT 必須化する
|
- Gemini 2.5-flash 503 フォールバック(→ 2.0-flash)を `archiveShare` + `rearchive` に追加済み
|
||||||
- **廃止前確認**: Together クライアント側が全リクエストで JWT を送っていることをログで確認してから削除
|
|
||||||
|
|
||||||
## 次にやること(優先順)
|
## 次にやること(優先順)
|
||||||
|
|
||||||
|
|
|
||||||
54
server.js
54
server.js
|
|
@ -807,7 +807,10 @@ async function togetherEnsureMember(pool, res, groupId, username, jwtUserId) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (jwtUserId) {
|
if (!jwtUserId) {
|
||||||
|
res.status(401).json({ error: '認証が必要です' });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const strict = await pool.query(
|
const strict = await pool.query(
|
||||||
`SELECT 1 FROM together_members m
|
`SELECT 1 FROM together_members m
|
||||||
WHERE m.group_id = $1 AND (
|
WHERE m.group_id = $1 AND (
|
||||||
|
|
@ -820,29 +823,8 @@ async function togetherEnsureMember(pool, res, groupId, username, jwtUserId) {
|
||||||
[gidNum, jwtUserId, usernames]
|
[gidNum, jwtUserId, usernames]
|
||||||
);
|
);
|
||||||
if (strict.rows.length > 0) return true;
|
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: 'グループのメンバーではありません' });
|
res.status(403).json({ error: 'グループのメンバーではありません' });
|
||||||
return false;
|
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]
|
|
||||||
);
|
|
||||||
if (legacyOnly.rows.length === 0) {
|
|
||||||
res.status(403).json({ error: 'グループのメンバーではありません' });
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[Together] togetherEnsureMember', e.message);
|
console.error('[Together] togetherEnsureMember', e.message);
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
|
@ -2629,12 +2611,13 @@ ${excerpt}
|
||||||
let summary = null;
|
let summary = null;
|
||||||
let tags = [];
|
let tags = [];
|
||||||
if (togetherGenAI && fullContent) {
|
if (togetherGenAI && fullContent) {
|
||||||
try {
|
|
||||||
// 最初の ## 見出し以降を本文とみなし 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 = 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 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 model = togetherGenAI.getGenerativeModel({ model: modelName });
|
||||||
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]);
|
||||||
const raw = result.response.text().trim();
|
const raw = result.response.text().trim();
|
||||||
|
|
@ -2645,9 +2628,13 @@ ${excerpt}
|
||||||
} catch {
|
} catch {
|
||||||
summary = raw.slice(0, 300);
|
summary = raw.slice(0, 300);
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
} catch (aiErr) {
|
} catch (aiErr) {
|
||||||
// Gemini 失敗(503等): Jina 本文は保存済みなので Reader は使える状態で done にする
|
console.error(`[together archive AI] ${modelName} share=${shareId}`, aiErr.message);
|
||||||
console.error('[together archive AI]', shareId, aiErr.message);
|
if (modelName === modelsToTry[modelsToTry.length - 1]) {
|
||||||
|
// 全モデル失敗: Jina 本文は保存済みなので Reader は使える状態で done にする
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2683,11 +2670,13 @@ ${excerpt}
|
||||||
if (!togetherGenAI) return res.status(503).json({ error: 'AI が設定されていません' });
|
if (!togetherGenAI) return res.status(503).json({ error: 'AI が設定されていません' });
|
||||||
await pool.query(`UPDATE together_shares SET archive_status='pending' WHERE id=$1`, [shareId]);
|
await pool.query(`UPDATE together_shares SET archive_status='pending' WHERE id=$1`, [shareId]);
|
||||||
res.json({ ok: true, status: 'pending' });
|
res.json({ ok: true, status: 'pending' });
|
||||||
try {
|
|
||||||
const bodyStart = share.full_content.search(/^#{1,2}\s/m);
|
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 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 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 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]);
|
||||||
const raw = result.response.text().trim();
|
const raw = result.response.text().trim();
|
||||||
|
|
@ -2695,8 +2684,13 @@ ${excerpt}
|
||||||
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)) : []; }
|
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); }
|
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]);
|
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) {
|
} catch (e) {
|
||||||
console.error('[rearchive gemini]', shareId, e.message);
|
console.error(`[rearchive ${modelName}]`, shareId, e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!rearchiveDone) {
|
||||||
await pool.query(`UPDATE together_shares SET archive_status='done' WHERE id=$1`, [shareId]);
|
await pool.query(`UPDATE together_shares SET archive_status='done' WHERE id=$1`, [shareId]);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue