Compare commits
No commits in common. "ce195cee72ed35090bed4e1379475a7a7c71b9ef" and "e935eb67340ac0afa91998ac7d1cb4873284792a" have entirely different histories.
ce195cee72
...
e935eb6734
|
|
@ -16,42 +16,24 @@ function shuffleInPlace(arr) {
|
||||||
function extractTermsFromBeginnerBox(box) {
|
function extractTermsFromBeginnerBox(box) {
|
||||||
const clone = box.cloneNode(true);
|
const clone = box.cloneNode(true);
|
||||||
clone.querySelector('.formula-label')?.remove();
|
clone.querySelector('.formula-label')?.remove();
|
||||||
|
const inner = clone.innerHTML.trim();
|
||||||
// <br> で行分割し、各行の先頭 <strong> だけを用語として扱う。
|
const parts = inner.split(/(?=<strong>)/i).map(s => s.trim()).filter(s => /^<strong>/i.test(s));
|
||||||
// 行中の inline <strong>(強調)は無視することで偽エントリを防ぐ。
|
|
||||||
const segments = [];
|
|
||||||
let cur = [];
|
|
||||||
for (const child of clone.childNodes) {
|
|
||||||
if (child.nodeName === 'BR') { segments.push(cur); cur = []; }
|
|
||||||
else cur.push(child);
|
|
||||||
}
|
|
||||||
if (cur.length) segments.push(cur);
|
|
||||||
|
|
||||||
const terms = [];
|
const terms = [];
|
||||||
for (const seg of segments) {
|
for (const p of parts) {
|
||||||
// 先頭ノードが <strong> でなければ(前に地の文がある)定義行ではない
|
const doc = new DOMParser().parseFromString('<div class="seg">'+p+'</div>', 'text/html');
|
||||||
let termNode = null;
|
const root = doc.querySelector('.seg');
|
||||||
const beforeNodes = [];
|
const st = root && root.querySelector('strong');
|
||||||
for (const node of seg) {
|
if (!st) continue;
|
||||||
if (node.nodeName === 'STRONG') { termNode = node; break; }
|
const term = st.textContent.trim();
|
||||||
beforeNodes.push(node);
|
let hint = '';
|
||||||
|
let n = st.nextSibling;
|
||||||
|
while (n) {
|
||||||
|
if (n.nodeType === 3) hint += n.textContent;
|
||||||
|
else if (n.nodeType === 1) hint += n.textContent;
|
||||||
|
n = n.nextSibling;
|
||||||
}
|
}
|
||||||
if (!termNode) continue;
|
hint = hint.replace(/^[::\s.]+/, '').trim();
|
||||||
// 先頭 <strong> の前に地の文があれば inline 強調とみなしてスキップ
|
if (term) terms.push({ term, hint });
|
||||||
if (beforeNodes.some(n => (n.textContent || '').trim())) continue;
|
|
||||||
|
|
||||||
const term = termNode.textContent.trim();
|
|
||||||
if (!term) continue;
|
|
||||||
|
|
||||||
// termNode 以降の全ノードの textContent を結合してヒントにする
|
|
||||||
const afterIdx = seg.indexOf(termNode) + 1;
|
|
||||||
const hint = seg.slice(afterIdx)
|
|
||||||
.map(n => n.textContent || '')
|
|
||||||
.join('')
|
|
||||||
.replace(/^[::\s.。=は]+/, '')
|
|
||||||
.trim();
|
|
||||||
|
|
||||||
terms.push({ term, hint });
|
|
||||||
}
|
}
|
||||||
return terms;
|
return terms;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// posimai-sc SW — same-origin の静的資産のみキャッシュ(CDN は対象外)
|
// posimai-sc SW — same-origin の静的資産のみキャッシュ(CDN は対象外)
|
||||||
const CACHE = 'posimai-sc-v9';
|
const CACHE = 'posimai-sc-v8';
|
||||||
const STATIC = [
|
const STATIC = [
|
||||||
'/',
|
'/',
|
||||||
'/index.html',
|
'/index.html',
|
||||||
|
|
|
||||||
44
server.js
44
server.js
|
|
@ -192,9 +192,6 @@ pool.on('error', (err) => {
|
||||||
// ── Gemini ────────────────────────────────
|
// ── Gemini ────────────────────────────────
|
||||||
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 アーカイブ専用キー(未設定時は genAI にフォールバック)
|
|
||||||
const togetherGenAI = process.env.TOGETHER_GEMINI_API_KEY
|
|
||||||
? new GoogleGenerativeAI(process.env.TOGETHER_GEMINI_API_KEY) : genAI;
|
|
||||||
|
|
||||||
|
|
||||||
// ── API Key 認証 ──────────────────────────
|
// ── API Key 認証 ──────────────────────────
|
||||||
|
|
@ -1987,43 +1984,6 @@ ${excerpt}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// POST /ai/generate — 汎用 Gemini プロキシ(Digest / Think 用。認証必須・レート制限あり)
|
|
||||||
r.post('/ai/generate', authMiddleware, async (req, res) => {
|
|
||||||
if (!genAI) return res.status(503).json({ error: 'AI not configured' });
|
|
||||||
if (!checkRateLimit('ai_generate', req.userId, 30, 60 * 60 * 1000)) {
|
|
||||||
return res.status(429).json({ error: 'AI利用回数が上限に達しました。1時間後に再試行してください' });
|
|
||||||
}
|
|
||||||
const { contents, systemPrompt, config = {} } = req.body || {};
|
|
||||||
if (!Array.isArray(contents) || !contents.length) {
|
|
||||||
return res.status(400).json({ error: 'contents required' });
|
|
||||||
}
|
|
||||||
if (JSON.stringify(contents).length > 60000) {
|
|
||||||
return res.status(400).json({ error: 'contents too large' });
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const modelOpts = { model: 'gemini-2.5-flash' };
|
|
||||||
if (systemPrompt && typeof systemPrompt === 'string') {
|
|
||||||
modelOpts.systemInstruction = systemPrompt;
|
|
||||||
}
|
|
||||||
const model = genAI.getGenerativeModel(modelOpts);
|
|
||||||
const generationConfig = {
|
|
||||||
maxOutputTokens: Math.min(config.maxOutputTokens || 500, 1000),
|
|
||||||
temperature: config.temperature ?? 0.5
|
|
||||||
};
|
|
||||||
const timeoutPromise = new Promise((_, reject) =>
|
|
||||||
setTimeout(() => reject(new Error('timeout')), 15000)
|
|
||||||
);
|
|
||||||
const result = await Promise.race([
|
|
||||||
model.generateContent({ contents, generationConfig }),
|
|
||||||
timeoutPromise
|
|
||||||
]);
|
|
||||||
res.json({ text: result.response.text() });
|
|
||||||
} catch (e) {
|
|
||||||
console.error('[ai/generate]', e.message);
|
|
||||||
res.status(500).json({ error: 'AI generation failed' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
r.post('/journal/upload', authMiddleware, (req, res) => {
|
r.post('/journal/upload', authMiddleware, (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { base64 } = req.body || {};
|
const { base64 } = req.body || {};
|
||||||
|
|
@ -2622,12 +2582,12 @@ ${excerpt}
|
||||||
|
|
||||||
let summary = null;
|
let summary = null;
|
||||||
let tags = [];
|
let tags = [];
|
||||||
if (togetherGenAI && fullContent) {
|
if (genAI && fullContent) {
|
||||||
try {
|
try {
|
||||||
// 最初の ## 見出し以降を本文とみなし 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 = togetherGenAI.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]);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue