dsh-paper-search 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +99 -1
- package/lib/sources.js +175 -40
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
| dsh-paper-search | DeepSeek Harness | Diuji pada |
|
|
6
6
|
|---|---|---|
|
|
7
|
-
| 0.1.
|
|
7
|
+
| 0.1.4 | 0.1.7-rc.2 | 28 Sep 2026, Windows, Node 24 |
|
|
8
8
|
| ~~0.1.0~~ | — | **JANGAN DIPAKAI — plugin gagal dimuat, lihat 0.1.1** |
|
|
9
9
|
|
|
10
10
|
Paket ini tidak memakai API internal DSH selain `ctx.tools.register` dan
|
|
@@ -12,6 +12,104 @@ Paket ini tidak memakai API internal DSH selain `ctx.tools.register` dan
|
|
|
12
12
|
berjalan di profil ini (`dsh-search-cascade`), jadi titik patahnya sama dan sudah
|
|
13
13
|
diketahui.
|
|
14
14
|
|
|
15
|
+
## [0.1.4] - 2026-09-28
|
|
16
|
+
|
|
17
|
+
Empat perbaikan yang **dipelajari dari Issues & PR repo upstream
|
|
18
|
+
`openags/paper-search-mcp`** — masing-masing menunjuk issue-nya, bukan dikarang.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- **PubMed & PMC diurutkan menurut TANGGAL, bukan relevansi** (upstream issue #29).
|
|
23
|
+
`esearch` tidak pernah diberi `sort`, dan bawaan PubMed adalah tanggal-terbaru.
|
|
24
|
+
Untuk kueri luas itu buruk: yang muncul paper baru yang tangensial, bukan paper
|
|
25
|
+
fondasional. Sekarang memakai `sort=relevance`.
|
|
26
|
+
|
|
27
|
+
Terukur pada `"ocular microbiome"`: puncaknya berubah dari paper 2026 yang
|
|
28
|
+
tangensial menjadi *The Microbiome, Ocular Surface, and Corneal Disorders*
|
|
29
|
+
(2023) dan *The Ocular Microbiome in Stevens-Johnson Syndrome* (2021) — persis
|
|
30
|
+
seperti yang diprediksi issue itu.
|
|
31
|
+
|
|
32
|
+
- **Nama penulis bisa terpecah PER HURUF** (upstream issue #109/#110/#112).
|
|
33
|
+
Konektor mengoper **string** ke medan bertipe daftar, lalu pemakainya
|
|
34
|
+
meng-`join` string itu — `"TAN, Kwan Hong"` menjadi
|
|
35
|
+
`"T; A; N; ,; ; K; w; a; n; …"`. Ditambahkan `daftarPenulis()` yang menerima
|
|
36
|
+
string, array string, array objek (`{name}`, `{display_name}`,
|
|
37
|
+
`{given,family}`), bentuk bersarang OpenAlex (`{author:{display_name}}`), dan
|
|
38
|
+
medan yang hilang — **tidak pernah melempar**.
|
|
39
|
+
|
|
40
|
+
`daftarPenulis()` **sengaja TIDAK memecah pada koma**: `"TAN, Kwan Hong"`
|
|
41
|
+
adalah SATU penulis. Hanya titik koma yang jadi pemisah. Konektor yang
|
|
42
|
+
sumbernya memang memakai daftar berpemisah koma (DOAJ `authorString`, IACR)
|
|
43
|
+
memecahnya lebih dulu, baru menormalkan.
|
|
44
|
+
|
|
45
|
+
- **arXiv: kueri multi-kata kini presisi dulu, longgar sebagai cadangan**
|
|
46
|
+
(upstream issue #6/#101/#113). Bentuk `all:kata1 AND all:kata2` dipakai lebih
|
|
47
|
+
dulu; hanya kalau kosong, bentuk longgar dijalankan.
|
|
48
|
+
|
|
49
|
+
**Kenapa TIDAK sekadar meniru perbaikan upstream** yang mengutip frasa:
|
|
50
|
+
**diukur 28 Sep 2026** — untuk kueri Indonesia `"literasi digital"`, bentuk
|
|
51
|
+
terkutip dan bentuk AND sama-sama memberi **0 hasil**, sedangkan bentuk longgar
|
|
52
|
+
memberi **50**. arXiv adalah korpus berbahasa Inggris; mengutip membabi-buta
|
|
53
|
+
akan menghapus SELURUH hasil untuk topik non-Inggris. Jadi yang dipakai adalah
|
|
54
|
+
presisi-**dengan**-cadangan.
|
|
55
|
+
|
|
56
|
+
- **HTTP 406 arXiv kadang masih membawa Atom feed yang SAH** (upstream issue #121).
|
|
57
|
+
Sebelumnya 406 langsung dianggap gagal. Sekarang isi balasannya dicoba diurai
|
|
58
|
+
lebih dulu — issue itu melaporkan 406 dengan body 5250 byte berisi 2 entri sah.
|
|
59
|
+
Untuk itu `getText()` kini menyimpan `err.bodyPenuh` terpisah dari `err.body`
|
|
60
|
+
(yang tetap pendek untuk pesan galat).
|
|
61
|
+
|
|
62
|
+
### Added
|
|
63
|
+
|
|
64
|
+
- **15 asersi regresi baru**: 9 bentuk penulis (termasuk kasus bug #109), 3 bentuk
|
|
65
|
+
kueri arXiv, penjaga `sort=relevance`, dan penjaga penyelamatan body 406.
|
|
66
|
+
|
|
67
|
+
### Catatan — yang sudah ada dan TIDAK perlu diubah
|
|
68
|
+
|
|
69
|
+
Diperiksa terhadap issue upstream, dan ternyata sudah tertangani sejak 0.1.0:
|
|
70
|
+
timeout 25 detik per permintaan (issue #55), pelaporan sumber tidak dikenal
|
|
71
|
+
(issue #105), dedup lintas sumber (issue #33), dan prinsip pelaporan kegagalan
|
|
72
|
+
alih-alih "0 hasil" (issue #100/#111).
|
|
73
|
+
|
|
74
|
+
**PR upstream #97** (`feat: DeepSeek Harness (dsh) integration`) memakai jalur
|
|
75
|
+
**MCP bridge** yang mengekspos **57 tool** — persis rute yang kami buang karena
|
|
76
|
+
biayanya **12.342 token per permintaan**. Plugin ini mendaftarkan **2 tool**.
|
|
77
|
+
Jadi PR itu mengukuhkan keputusan kami, bukan mengubahnya.
|
|
78
|
+
|
|
79
|
+
## [0.1.3] - 2026-09-28
|
|
80
|
+
|
|
81
|
+
### Fixed
|
|
82
|
+
|
|
83
|
+
- **Portal Indonesia melaporkan GAGAL padahal hasilnya cuma KOSONG.** Ketiga
|
|
84
|
+
konektor Indonesia melempar `markup ... berubah` setiap kali penanda hasil tidak
|
|
85
|
+
ditemukan — termasuk saat portal itu **sah-sah saja menjawab nol hasil**. Itu
|
|
86
|
+
keliru dengan arah berlawanan dari prinsip paket ini: melaporkan kegagalan di
|
|
87
|
+
tempat yang seharusnya kosong sama menyesatkannya dengan menyembunyikan
|
|
88
|
+
kegagalan.
|
|
89
|
+
|
|
90
|
+
Ketahuan dari **uji orkestrasi**, bukan dari uji unit: `Sinta` gagal untuk
|
|
91
|
+
`"literasi digital remaja"` padahal berhasil untuk `"pendidikan karakter"`.
|
|
92
|
+
Setelah halaman kosong dan halaman ber-hasil dibandingkan, ternyata ketiga
|
|
93
|
+
portal punya penanda nol-hasilnya sendiri:
|
|
94
|
+
|
|
95
|
+
| Portal | Penanda nol hasil | Halaman kosong |
|
|
96
|
+
|---|---|---|
|
|
97
|
+
| Garuda | `Found 0 documents` | 11.181 huruf |
|
|
98
|
+
| Sinta | header `Results for "<q>"` tetap ada, tautan profil nol | 34.063 huruf |
|
|
99
|
+
| IOS | `No Results!` | 23.041 huruf |
|
|
100
|
+
|
|
101
|
+
`portalNolHasil()` sekarang membedakan **tiga** kemungkinan — nol hasil (→ `[]`),
|
|
102
|
+
markup berubah (→ error), dan diblokir (→ error) — bukan dua.
|
|
103
|
+
|
|
104
|
+
Akibatnya untuk Sinta: kueri spesifik yang memang nol hasil tidak lagi tampak
|
|
105
|
+
seperti kerusakan. Sinta mendaftar **jurnal**, bukan artikel, jadi nol hasil itu
|
|
106
|
+
wajar dan sekarang dilaporkan sebagai nol.
|
|
107
|
+
|
|
108
|
+
### Added
|
|
109
|
+
|
|
110
|
+
- 3 asersi regresi: kueri omong kosong ke ketiga portal Indonesia harus
|
|
111
|
+
menghasilkan **0 hasil tanpa error**.
|
|
112
|
+
|
|
15
113
|
## [0.1.2] - 2026-09-28
|
|
16
114
|
|
|
17
115
|
### Fixed
|
package/lib/sources.js
CHANGED
|
@@ -34,7 +34,11 @@ async function getText(url, { headers = {}, accept, timeoutMs = TIMEOUT_MS } = {
|
|
|
34
34
|
if (!res.ok) {
|
|
35
35
|
const err = new Error(`HTTP ${res.status}`);
|
|
36
36
|
err.status = res.status;
|
|
37
|
+
// `body` untuk PESAN (pendek). `bodyPenuh` disimpan TERPISAH supaya isi
|
|
38
|
+
// balasan yang masih berguna bisa diselamatkan — terukur di arXiv:
|
|
39
|
+
// HTTP 406 kadang justru membawa Atom feed yang SAH (issue #121).
|
|
37
40
|
err.body = teks.slice(0, 160).replace(/\s+/g, ' ');
|
|
41
|
+
err.bodyPenuh = teks.slice(0, 512 * 1024);
|
|
38
42
|
throw err;
|
|
39
43
|
}
|
|
40
44
|
return teks;
|
|
@@ -66,6 +70,47 @@ function potong(s, n) {
|
|
|
66
70
|
return t.length > n ? `${t.slice(0, n - 1)}…` : t;
|
|
67
71
|
}
|
|
68
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Normalkan daftar penulis dari bentuk apa pun menjadi ARRAY nama. Tidak pernah
|
|
75
|
+
* melempar; selalu mengembalikan array (boleh kosong).
|
|
76
|
+
*
|
|
77
|
+
* Ini jawaban atas tiga bug upstream yang sudah benar-benar terjadi
|
|
78
|
+
* (issue #109, #110, #112): konektor mengoper **string** ke medan yang bertipe
|
|
79
|
+
* daftar, lalu pemakainya meng-`join` string itu sehingga setiap nama terpecah
|
|
80
|
+
* per huruf — `"TAN, Kwan Hong"` menjadi `"T; A; N; ,; ; K; w; a; n; …"`.
|
|
81
|
+
* Bentuk lain yang juga muncul di lapangan: array objek (`{name}`,
|
|
82
|
+
* `{display_name}`, `{given, family}`), dan medan yang hilang sama sekali.
|
|
83
|
+
*
|
|
84
|
+
* SENGAJA TIDAK memecah pada KOMA: `"TAN, Kwan Hong"` adalah SATU penulis, dan
|
|
85
|
+
* memecahnya justru merusak nama. Hanya titik koma yang dipakai sebagai pemisah.
|
|
86
|
+
*/
|
|
87
|
+
export function daftarPenulis(x) {
|
|
88
|
+
const keluaran = [];
|
|
89
|
+
const tambah = (v) => {
|
|
90
|
+
if (v === null || v === undefined) return;
|
|
91
|
+
if (Array.isArray(v)) { v.forEach(tambah); return; }
|
|
92
|
+
if (typeof v === 'object') {
|
|
93
|
+
// Bentuk bersarang yang nyata di lapangan: OpenAlex membungkus penulis
|
|
94
|
+
// sebagai `{ author: { display_name } }`.
|
|
95
|
+
if (v.author) { tambah(v.author); return; }
|
|
96
|
+
tambah(v.name ?? v.display_name ?? v.raw_author_name ?? v.fullName
|
|
97
|
+
?? [v.given, v.family].filter(Boolean).join(' '));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (typeof v === 'string') {
|
|
101
|
+
for (const bagian of v.split(';')) {
|
|
102
|
+
const b = bersihTeks(bagian);
|
|
103
|
+
if (b) keluaran.push(b);
|
|
104
|
+
}
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const s = bersihTeks(v);
|
|
108
|
+
if (s) keluaran.push(s);
|
|
109
|
+
};
|
|
110
|
+
tambah(x);
|
|
111
|
+
return keluaran.slice(0, 50);
|
|
112
|
+
}
|
|
113
|
+
|
|
69
114
|
/** DOI dinormalkan tanpa awalan URL — supaya dedup bekerja. */
|
|
70
115
|
function normDoi(doi) {
|
|
71
116
|
if (!doi) return null;
|
|
@@ -132,7 +177,7 @@ function buatPaper(p) {
|
|
|
132
177
|
const doi = normDoi(p.doi);
|
|
133
178
|
return {
|
|
134
179
|
title: potong(runtuhkanJudul(bersihTeks(p.title) ?? ''), 300),
|
|
135
|
-
authors:
|
|
180
|
+
authors: daftarPenulis(p.authors).slice(0, 15),
|
|
136
181
|
year: Number.isFinite(Number(p.year)) && Number(p.year) > 1500 ? Number(p.year) : null,
|
|
137
182
|
source: p.source,
|
|
138
183
|
doi,
|
|
@@ -154,7 +199,7 @@ async function crossref(query, { rows, email }) {
|
|
|
154
199
|
const j = await getJson(u.toString());
|
|
155
200
|
return (j?.message?.items ?? []).map((it) => buatPaper({
|
|
156
201
|
title: it.title?.[0],
|
|
157
|
-
authors: (it.author
|
|
202
|
+
authors: daftarPenulis(it.author),
|
|
158
203
|
year: it.issued?.['date-parts']?.[0]?.[0],
|
|
159
204
|
source: 'crossref',
|
|
160
205
|
doi: it.DOI,
|
|
@@ -180,7 +225,7 @@ async function openalex(query, { rows, email }) {
|
|
|
180
225
|
const j = await getJson(u.toString());
|
|
181
226
|
return (j?.results ?? []).map((w) => buatPaper({
|
|
182
227
|
title: w.title,
|
|
183
|
-
authors: (w.authorships
|
|
228
|
+
authors: daftarPenulis(w.authorships),
|
|
184
229
|
year: w.publication_year,
|
|
185
230
|
source: 'openalex',
|
|
186
231
|
doi: w.doi,
|
|
@@ -198,7 +243,7 @@ async function doaj(query, { rows }) {
|
|
|
198
243
|
const b = r.bibjson ?? {};
|
|
199
244
|
return buatPaper({
|
|
200
245
|
title: b.title,
|
|
201
|
-
authors: (b.author
|
|
246
|
+
authors: daftarPenulis(b.author),
|
|
202
247
|
year: b.year,
|
|
203
248
|
source: 'doaj',
|
|
204
249
|
doi: (b.identifier ?? []).find((x) => x.type === 'doi')?.id,
|
|
@@ -215,7 +260,10 @@ async function europepmc(query, { rows }) {
|
|
|
215
260
|
const j = await getJson(u);
|
|
216
261
|
return (j?.resultList?.result ?? []).map((r) => buatPaper({
|
|
217
262
|
title: r.title,
|
|
218
|
-
|
|
263
|
+
// DOAJ memberi `authorString` yang MEMANG dipisah koma ("A, B, C"), jadi
|
|
264
|
+
// pemecahan dilakukan di sini dulu — `daftarPenulis` sendiri sengaja tidak
|
|
265
|
+
// memecah koma, supaya "TAN, Kwan Hong" (satu penulis) tidak rusak.
|
|
266
|
+
authors: daftarPenulis((r.authorString ?? '').split(/,\s*/)),
|
|
219
267
|
year: r.pubYear,
|
|
220
268
|
source: 'europepmc',
|
|
221
269
|
doi: r.doi,
|
|
@@ -229,7 +277,7 @@ async function europepmc(query, { rows }) {
|
|
|
229
277
|
async function pubmed(query, { rows }) {
|
|
230
278
|
// E-utilities butuh DUA panggilan: esearch (id) lalu esummary (metadata).
|
|
231
279
|
const base = 'https://eutils.ncbi.nlm.nih.gov/entrez/eutils';
|
|
232
|
-
const s = await getJson(`${base}/esearch.fcgi?db=pubmed&retmode=json&retmax=${rows}&term=${encodeURIComponent(query)}`);
|
|
280
|
+
const s = await getJson(`${base}/esearch.fcgi?db=pubmed&retmode=json&retmax=${rows}&sort=relevance&term=${encodeURIComponent(query)}`);
|
|
233
281
|
const ids = s?.esearchresult?.idlist ?? [];
|
|
234
282
|
if (ids.length === 0) return [];
|
|
235
283
|
const d = await getJson(`${base}/esummary.fcgi?db=pubmed&retmode=json&id=${ids.join(',')}`);
|
|
@@ -238,7 +286,7 @@ async function pubmed(query, { rows }) {
|
|
|
238
286
|
if (!r) return null;
|
|
239
287
|
return buatPaper({
|
|
240
288
|
title: r.title,
|
|
241
|
-
authors: (r.authors
|
|
289
|
+
authors: daftarPenulis(r.authors),
|
|
242
290
|
year: String(r.pubdate ?? '').slice(0, 4),
|
|
243
291
|
source: 'pubmed',
|
|
244
292
|
doi: (r.articleids ?? []).find((a) => a.idtype === 'doi')?.value,
|
|
@@ -251,32 +299,89 @@ async function pubmed(query, { rows }) {
|
|
|
251
299
|
// arXiv meminta TIDAK LEBIH dari 1 permintaan / 3 detik (TOU mereka). Karena
|
|
252
300
|
// sumber dijalankan paralel, permintaan arXiv diserialkan sendiri di sini.
|
|
253
301
|
let arxivTerakhir = 0;
|
|
254
|
-
async function
|
|
302
|
+
async function arxivTunggu() {
|
|
255
303
|
const jeda = 3000 - (Date.now() - arxivTerakhir);
|
|
256
304
|
if (jeda > 0) await new Promise((r) => setTimeout(r, jeda));
|
|
305
|
+
arxivTerakhir = Date.now();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** Uraikan Atom feed arXiv menjadi daftar paper. */
|
|
309
|
+
function uraikanArxiv(xml, rows) {
|
|
310
|
+
const out = [];
|
|
311
|
+
for (const m of xml.matchAll(/<entry>([\s\S]*?)<\/entry>/g)) {
|
|
312
|
+
const blok = m[1];
|
|
313
|
+
const ambil = (tag) => {
|
|
314
|
+
const r = new RegExp(`<${tag}[^>]*>([\\s\\S]*?)</${tag}>`).exec(blok);
|
|
315
|
+
return r ? r[1] : null;
|
|
316
|
+
};
|
|
317
|
+
out.push(buatPaper({
|
|
318
|
+
title: ambil('title'),
|
|
319
|
+
authors: [...blok.matchAll(/<author>\s*<name>([^<]+)<\/name>/g)].map((x) => x[1]),
|
|
320
|
+
year: String(ambil('published') ?? '').slice(0, 4),
|
|
321
|
+
source: 'arxiv',
|
|
322
|
+
doi: ambil('arxiv:doi'),
|
|
323
|
+
url: ambil('id'),
|
|
324
|
+
journal: 'arXiv (preprint)',
|
|
325
|
+
abstract: ambil('summary'),
|
|
326
|
+
openAccess: true,
|
|
327
|
+
}));
|
|
328
|
+
}
|
|
329
|
+
return out.filter((p) => p.title).slice(0, rows);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Ambil Atom feed arXiv, dengan DUA perbaikan yang diambil dari issue upstream:
|
|
334
|
+
*
|
|
335
|
+
* 1. **HTTP 406 kadang masih membawa feed yang SAH** (issue #121). Dilaporkan
|
|
336
|
+
* terukur: 406 dengan body 5250 byte ber-`Content-Type: application/atom+xml`
|
|
337
|
+
* berisi 2 entri. Jadi 406 tidak langsung digagalkan — isinya dicoba dibaca
|
|
338
|
+
* dulu. Kalau body-nya benar-benar kosong, barulah itu kegagalan sungguhan.
|
|
339
|
+
* 2. **Kueri multi-kata tanpa kutip bisa MENGGANTUNG** di backend arXiv
|
|
340
|
+
* (issue #101). Karena itu bentuk presisi dipakai lebih dulu (lihat `arxiv`).
|
|
341
|
+
*/
|
|
342
|
+
async function ambilArxiv(kueri, rows) {
|
|
343
|
+
const u = `https://export.arxiv.org/api/query?search_query=${encodeURIComponent(kueri)}&max_results=${rows}&sortBy=relevance`;
|
|
257
344
|
try {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
345
|
+
return uraikanArxiv(await getText(u, { accept: 'application/atom+xml' }), rows);
|
|
346
|
+
} catch (e) {
|
|
347
|
+
if (e.status === 406 && /<entry>/.test(e.bodyPenuh ?? '')) return uraikanArxiv(e.bodyPenuh, rows);
|
|
348
|
+
throw e;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Bentuk kueri arXiv. Presisi dulu (`all:kata1 AND all:kata2`), longgar
|
|
354
|
+
* (`all:kata1 kata2`) sebagai cadangan.
|
|
355
|
+
*
|
|
356
|
+
* Kenapa TIDAK sekadar meniru perbaikan upstream yang mengutip frasa:
|
|
357
|
+
* **diukur 28 Sep 2026** — untuk kueri Indonesia `"literasi digital"`, bentuk
|
|
358
|
+
* terkutip dan bentuk AND sama-sama memberi **0 hasil**, sedangkan bentuk
|
|
359
|
+
* longgar memberi **50**. arXiv memang korpus berbahasa Inggris. Mengutip buta
|
|
360
|
+
* akan menghapus SELURUH hasil untuk topik non-Inggris, jadi yang dipakai adalah
|
|
361
|
+
* presisi-dengan-cadangan, bukan kutip-membabi-buta.
|
|
362
|
+
*/
|
|
363
|
+
export function kueriArxiv(query, presisi) {
|
|
364
|
+
const kata = String(query).trim().split(/\s+/).filter(Boolean);
|
|
365
|
+
if (kata.length === 0) return `all:${query}`;
|
|
366
|
+
if (kata.length === 1) return `all:${kata[0]}`;
|
|
367
|
+
return presisi ? kata.map((k) => `all:${k}`).join(' AND ') : `all:${kata.join(' ')}`;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async function arxiv(query, { rows }) {
|
|
371
|
+
await arxivTunggu();
|
|
372
|
+
let hasil;
|
|
373
|
+
try {
|
|
374
|
+
hasil = await ambilArxiv(kueriArxiv(query, true), rows);
|
|
375
|
+
} finally {
|
|
376
|
+
arxivTerakhir = Date.now();
|
|
377
|
+
}
|
|
378
|
+
if (hasil.length > 0) return hasil;
|
|
379
|
+
|
|
380
|
+
// Cadangan longgar. Hanya dijalankan kalau bentuk presisi benar-benar kosong,
|
|
381
|
+
// supaya permintaan arXiv tetap hemat (TOU mereka: 1 permintaan / 3 detik).
|
|
382
|
+
await arxivTunggu();
|
|
383
|
+
try {
|
|
384
|
+
return await ambilArxiv(kueriArxiv(query, false), rows);
|
|
280
385
|
} finally {
|
|
281
386
|
arxivTerakhir = Date.now();
|
|
282
387
|
}
|
|
@@ -287,7 +392,7 @@ async function semantic(query, { rows, apiKey }) {
|
|
|
287
392
|
const j = await getJson(u, apiKey ? { headers: { 'x-api-key': apiKey } } : {});
|
|
288
393
|
return (j?.data ?? []).map((d) => buatPaper({
|
|
289
394
|
title: d.title,
|
|
290
|
-
authors: (d.authors
|
|
395
|
+
authors: daftarPenulis(d.authors),
|
|
291
396
|
year: d.year,
|
|
292
397
|
source: 'semantic',
|
|
293
398
|
doi: d.externalIds?.DOI,
|
|
@@ -307,7 +412,7 @@ async function pmc(query, { rows }) {
|
|
|
307
412
|
// PubMed Central memakai E-utilities yang sama dengan PubMed:
|
|
308
413
|
// esearch (id) lalu esummary (metadata).
|
|
309
414
|
const base = 'https://eutils.ncbi.nlm.nih.gov/entrez/eutils';
|
|
310
|
-
const s = await getJson(`${base}/esearch.fcgi?db=pmc&retmode=json&retmax=${rows}&term=${encodeURIComponent(query)}`);
|
|
415
|
+
const s = await getJson(`${base}/esearch.fcgi?db=pmc&retmode=json&retmax=${rows}&sort=relevance&term=${encodeURIComponent(query)}`);
|
|
311
416
|
const ids = s?.esearchresult?.idlist ?? [];
|
|
312
417
|
if (ids.length === 0) return [];
|
|
313
418
|
const d = await getJson(`${base}/esummary.fcgi?db=pmc&retmode=json&id=${ids.join(',')}`);
|
|
@@ -317,7 +422,7 @@ async function pmc(query, { rows }) {
|
|
|
317
422
|
const pmcid = (r.articleids ?? []).find((a) => a.idtype === 'pmcid')?.value ?? `PMC${id}`;
|
|
318
423
|
return buatPaper({
|
|
319
424
|
title: r.title,
|
|
320
|
-
authors: (r.authors
|
|
425
|
+
authors: daftarPenulis(r.authors),
|
|
321
426
|
year: String(r.pubdate ?? '').slice(0, 4),
|
|
322
427
|
source: 'pmc',
|
|
323
428
|
doi: (r.articleids ?? []).find((a) => a.idtype === 'doi')?.value,
|
|
@@ -341,7 +446,7 @@ async function core(query, { rows, coreApiKey }) {
|
|
|
341
446
|
});
|
|
342
447
|
return (j?.results ?? []).map((c) => buatPaper({
|
|
343
448
|
title: c.title,
|
|
344
|
-
authors: (c.authors
|
|
449
|
+
authors: daftarPenulis(c.authors),
|
|
345
450
|
year: c.yearPublished,
|
|
346
451
|
source: 'core',
|
|
347
452
|
doi: c.doi,
|
|
@@ -356,7 +461,7 @@ async function zenodo(query, { rows }) {
|
|
|
356
461
|
const j = await getJson(`https://zenodo.org/api/records?q=${encodeURIComponent(query)}&size=${rows}`);
|
|
357
462
|
return (j?.hits?.hits ?? []).map((z) => buatPaper({
|
|
358
463
|
title: z.metadata?.title,
|
|
359
|
-
authors: (z.metadata?.creators
|
|
464
|
+
authors: daftarPenulis(z.metadata?.creators),
|
|
360
465
|
year: String(z.metadata?.publication_date ?? '').slice(0, 4),
|
|
361
466
|
source: 'zenodo',
|
|
362
467
|
doi: z.metadata?.doi ?? z.doi,
|
|
@@ -372,7 +477,7 @@ async function hal(query, { rows }) {
|
|
|
372
477
|
const j = await getJson(`https://api.archives-ouvertes.fr/search/?q=${encodeURIComponent(query)}&wt=json&rows=${rows}&fl=${fl}`);
|
|
373
478
|
return (j?.response?.docs ?? []).map((a) => buatPaper({
|
|
374
479
|
title: pertama(a.title_s),
|
|
375
|
-
authors: a.authFullName_s
|
|
480
|
+
authors: daftarPenulis(a.authFullName_s),
|
|
376
481
|
year: a.producedDateY_i,
|
|
377
482
|
source: 'hal',
|
|
378
483
|
doi: pertama(a.doiId_s),
|
|
@@ -400,7 +505,7 @@ async function iacr(query, { rows }) {
|
|
|
400
505
|
const id = tautan[i][1];
|
|
401
506
|
out.push(buatPaper({
|
|
402
507
|
title: ambil(/<strong>([\s\S]*?)<\/strong>/),
|
|
403
|
-
authors: (ambil(/<span class="fst-italic">([\s\S]*?)<\/span>/) ?? '').split(/,\s*/)
|
|
508
|
+
authors: daftarPenulis((ambil(/<span class="fst-italic">([\s\S]*?)<\/span>/) ?? '').split(/,\s*/)),
|
|
404
509
|
year: id.slice(0, 4),
|
|
405
510
|
source: 'iacr',
|
|
406
511
|
url: `https://eprint.iacr.org/${id}`,
|
|
@@ -430,7 +535,7 @@ async function unpaywall(query, { rows, email }) {
|
|
|
430
535
|
const oa = j.best_oa_location ?? null;
|
|
431
536
|
const hasil = buatPaper({
|
|
432
537
|
title: j.title,
|
|
433
|
-
authors: (j.z_authors
|
|
538
|
+
authors: daftarPenulis(j.z_authors),
|
|
434
539
|
year: j.year,
|
|
435
540
|
source: 'unpaywall',
|
|
436
541
|
doi: j.doi ?? doi,
|
|
@@ -459,6 +564,29 @@ function ambilHtml(html, pola, batas) {
|
|
|
459
564
|
return out;
|
|
460
565
|
}
|
|
461
566
|
|
|
567
|
+
/**
|
|
568
|
+
* Saat penanda hasil tidak ditemukan, ada TIGA kemungkinan yang BERBEDA:
|
|
569
|
+
* 1. portal menjawab "nol hasil" -> `[]`, itu hasil yang SAH
|
|
570
|
+
* 2. markup portal berubah -> error
|
|
571
|
+
* 3. portal memblokir/mengalihkan -> error
|
|
572
|
+
*
|
|
573
|
+
* Sebelum ini (1) dan (2) diperlakukan sama, sehingga portal yang benar-benar
|
|
574
|
+
* kosong dilaporkan sebagai **GAGAL**. Itu keliru dengan arah berlawanan:
|
|
575
|
+
* melaporkan kegagalan padahal hasilnya sah-sah saja kosong — sama
|
|
576
|
+
* menyesatkannya dengan menyembunyikan kegagalan.
|
|
577
|
+
*
|
|
578
|
+
* Penanda nol-hasil diukur 28 Sep 2026 memakai kueri omong kosong, lalu
|
|
579
|
+
* dibandingkan dengan kueri yang ada hasilnya:
|
|
580
|
+
*
|
|
581
|
+
* Garuda : "Found 0 documents" (halaman 11.181 huruf)
|
|
582
|
+
* Sinta : header `Results for "<q>"` tetap ada, tautan profil nol
|
|
583
|
+
* (halaman 34.063 huruf) — Sinta TIDAK menulis pesan kosong
|
|
584
|
+
* IOS : "No Results!" (halaman 23.041 huruf)
|
|
585
|
+
*/
|
|
586
|
+
function portalNolHasil(html, ...penanda) {
|
|
587
|
+
return penanda.some((re) => re.test(html));
|
|
588
|
+
}
|
|
589
|
+
|
|
462
590
|
const GARUDA_BASE = 'https://garuda.kemdiktisaintek.go.id';
|
|
463
591
|
|
|
464
592
|
/** Garuda — indeks artikel jurnal Indonesia (Kemdiktisaintek). */
|
|
@@ -472,9 +600,10 @@ async function garuda(query, { rows }) {
|
|
|
472
600
|
journal: 'Garuda (jurnal Indonesia)',
|
|
473
601
|
openAccess: true,
|
|
474
602
|
})).filter((p) => p.title);
|
|
475
|
-
// Penguraian HTML yang tidak menemukan satu pun pola
|
|
476
|
-
//
|
|
603
|
+
// Penguraian HTML yang tidak menemukan satu pun pola bisa berarti DUA hal yang
|
|
604
|
+
// berlawanan. Bedakan: Garuda menulis "Found 0 documents" saat memang kosong.
|
|
477
605
|
if (hasil.length === 0 && !/title-article/.test(html)) {
|
|
606
|
+
if (portalNolHasil(html, /Found\s+0\s+document/i)) return [];
|
|
478
607
|
throw new Error('markup Garuda berubah: penanda "title-article" tidak ditemukan lagi');
|
|
479
608
|
}
|
|
480
609
|
return hasil;
|
|
@@ -491,7 +620,12 @@ async function sinta(query, { rows }) {
|
|
|
491
620
|
journal: 'Sinta (jurnal terakreditasi)',
|
|
492
621
|
openAccess: true,
|
|
493
622
|
})).filter((p) => p.title);
|
|
623
|
+
// Sinta TIDAK menulis pesan kosong — ia tetap menampilkan header
|
|
624
|
+
// `Results for "<kueri>"` lalu daftar yang kosong. Jadi header itu yang
|
|
625
|
+
// dipakai sebagai bukti "halaman dimuat, hasilnya memang nol".
|
|
626
|
+
// (Sinta mendaftar JURNAL, bukan artikel, jadi kueri spesifik memang sering nol.)
|
|
494
627
|
if (hasil.length === 0 && !/journals\/profile\//.test(html)) {
|
|
628
|
+
if (portalNolHasil(html, /Results for/i)) return [];
|
|
495
629
|
throw new Error('markup Sinta berubah: tautan "journals/profile" tidak ditemukan lagi');
|
|
496
630
|
}
|
|
497
631
|
return hasil;
|
|
@@ -509,6 +643,7 @@ async function ios(query, { rows }) {
|
|
|
509
643
|
openAccess: true,
|
|
510
644
|
})).filter((p) => p.title);
|
|
511
645
|
if (hasil.length === 0 && !/class="title"/.test(html)) {
|
|
646
|
+
if (portalNolHasil(html, /No Results!/i)) return [];
|
|
512
647
|
throw new Error('markup IOS berubah: tautan hasil ber-class "title" tidak ditemukan lagi');
|
|
513
648
|
}
|
|
514
649
|
return hasil;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-paper-search",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Literature search for DeepSeek Harness: thirteen international sources (Crossref, OpenAlex, DOAJ, Europe PMC, PubMed, PubMed Central, arXiv, Semantic Scholar, CORE, Zenodo, HAL, IACR ePrint, Unpaywall) and three Indonesian journal portals (Garuda, Sinta, IOS OneSearch), all native HTTP with no external CLI required, plus a status tool that reports which sources answer and which credentials are configured.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|