Repository navigation
Expand file tree
/
Copy pathreleases.js
More file actions
99 lines (91 loc) · 3.3 KB
/
Copy pathreleases.js
File metadata and controls
99 lines (91 loc) · 3.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
const RELEASES_URL = 'https://api.github.com/repos/BasedHardware/omi/releases?per_page=15';
let cache = { at: 0, notes: [] };
function words(text) {
return String(text || '')
.toLowerCase()
.replace(/[^a-z0-9\s]/g, ' ')
.split(/\s+/)
.filter((word) => word.length >= 3);
}
function wantsReleaseNote(text) {
const s = String(text || '');
if (/\b(release notes?|what'?s new|latest version|new version|firmware update)\b/i.test(s)) return true;
return /\bdesktop\b/i.test(s) && /\b(download|install|version)\b/i.test(s);
}
function clip(text) {
const plain = String(text || '')
.replace(/!\[[^\]]*\]\([^)]+\)/g, ' ')
.replace(/\s+/g, ' ')
.trim();
return plain.length > 280 ? `${plain.slice(0, 280)}…` : plain;
}
function bestNote(question, notes, { onSources } = {}) {
const wanted = new Set(words(question));
let best = null;
let score = 0;
for (const note of notes || []) {
const shared = words(`${note.name} ${note.body}`).filter((word) => wanted.has(word)).length;
if (shared > score) {
best = note;
score = shared;
}
}
if (!best || score < 1) return '';
const releaseUrl = best.tag
? `https://github.com/BasedHardware/omi/releases/tag/${encodeURIComponent(best.tag)}`
: '';
onSources?.(releaseUrl ? [releaseUrl] : []);
return `${best.name} (${best.tag}). ${clip(best.body)} ${releaseUrl}`.trim();
}
async function loadNotes(fetchImpl, store) {
const now = Date.now();
if (cache.notes.length && now - cache.at < 60 * 60 * 1000) return cache.notes;
const fetchFn = fetchImpl || fetch;
try {
const res = await fetchFn(RELEASES_URL, {
headers: { Accept: 'application/vnd.github+json', 'User-Agent': 'omi-vector-bot' },
});
if (!res.ok) return cache.notes;
const data = await res.json();
const notes = (Array.isArray(data) ? data : []).map((item) => ({
tag: String(item.tag_name || ''),
name: String(item.name || item.tag_name || ''),
body: clip(item.body),
publishedAt: item.published_at || null,
}));
require('./answerTrace').registerPublishedSources(notes.filter(row=>row.tag).map(row=>`https://github.com/BasedHardware/omi/releases/tag/${encodeURIComponent(row.tag)}`));
cache = { at: now, notes };
if (store?.saveRelease) {
for (const note of notes) {
try {
await store.saveRelease(note);
} catch (err) {
console.error('[Releases] save failed:', err.message);
}
}
}
return notes;
} catch (err) {
console.error('[Releases] lookup failed:', err.message);
return cache.notes;
}
}
async function matchingRelease(question, { fetchImpl, store, onSources } = {}) {
if (!wantsReleaseNote(question)) return '';
const saved = store || (process.env.DATABASE_URL ? require('./db') : null);
const notes = await loadNotes(fetchImpl, saved);
const live = bestNote(question, notes, { onSources });
if (live) return live;
if (!saved?.searchReleases) return '';
try {
const rows = await saved.searchReleases(question);
return bestNote(question, rows, { onSources });
} catch (err) {
console.error('[Releases] stored lookup failed:', err.message);
return '';
}
}
function resetReleaseCache() {
cache = { at: 0, notes: [] };
}
module.exports = { wantsReleaseNote, matchingRelease, bestNote, resetReleaseCache };