@akredit/acik-mcp 0.1.0 → 0.5.0
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/README.md +94 -8
- package/dist/apps/acik-mcp/src/araclar.d.ts +2 -28
- package/dist/apps/acik-mcp/src/araclar.js +29 -114
- package/dist/apps/acik-mcp/src/bologna.d.ts +291 -0
- package/dist/apps/acik-mcp/src/bologna.js +1693 -0
- package/dist/apps/acik-mcp/src/genel-ayristir.d.ts +92 -0
- package/dist/apps/acik-mcp/src/genel-ayristir.js +489 -0
- package/dist/apps/acik-mcp/src/http.d.ts +31 -3
- package/dist/apps/acik-mcp/src/http.js +90 -17
- package/dist/apps/acik-mcp/src/index.d.ts +7 -0
- package/dist/apps/acik-mcp/src/index.js +7 -0
- package/dist/apps/acik-mcp/src/kaynak-dogrula.d.ts +94 -0
- package/dist/apps/acik-mcp/src/kaynak-dogrula.js +460 -0
- package/dist/apps/acik-mcp/src/kaynak.d.ts +72 -0
- package/dist/apps/acik-mcp/src/kaynak.js +155 -0
- package/dist/apps/acik-mcp/src/kisisel-veri.d.ts +16 -2
- package/dist/apps/acik-mcp/src/kisisel-veri.js +78 -15
- package/dist/apps/acik-mcp/src/not-tablosu.d.ts +39 -0
- package/dist/apps/acik-mcp/src/not-tablosu.js +268 -0
- package/dist/apps/acik-mcp/src/promptlar.d.ts +48 -0
- package/dist/apps/acik-mcp/src/promptlar.js +323 -0
- package/dist/apps/acik-mcp/src/sunucu.d.ts +12 -4
- package/dist/apps/acik-mcp/src/sunucu.js +108 -24
- package/dist/apps/acik-mcp/src/temel.d.ts +59 -0
- package/dist/apps/acik-mcp/src/temel.js +10 -0
- package/dist/apps/acik-mcp/src/yardim.d.ts +56 -0
- package/dist/apps/acik-mcp/src/yardim.js +381 -0
- package/dist/packages/baglayicilar/src/bologna-proliz/ayristir.d.ts +29 -0
- package/dist/packages/baglayicilar/src/bologna-proliz/ayristir.js +200 -0
- package/dist/packages/cekirdek/src/bologna/taslak.d.ts +19 -3
- package/dist/packages/cekirdek/src/bologna/taslak.js +70 -9
- package/dist/packages/cekirdek/src/turler.d.ts +8 -0
- package/package.json +5 -3
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
// Kaynak doğrulama: DOI, ISBN ya da başlık+yazar ile eserin gerçekten var olduğunu açık kataloglardan denetler.
|
|
2
|
+
// Yalnız sabit, izinli hostlara sorar (api.crossref.org, openlibrary.org, www.googleapis.com/books). Ağ adresi
|
|
3
|
+
// kullanıcıdan alınmaz; kullanıcı girdisi yalnız doğrulanmış DOI, ISBN ya da sorgu parametresi olarak, kodlanarak
|
|
4
|
+
// adrese girer (SSRF yok). Her istekte zaman aşımı ve yanıt boyutu sınırı vardır, yanıt zod ile doğrulanır.
|
|
5
|
+
//
|
|
6
|
+
// Uydurma yok: künye yalnız katalog yanıtındaki alanlardan kurulur ve kaynak_bicimle biçimiyle yazılır. Başlık
|
|
7
|
+
// ve yazarla aramada eşleşme deterministik bir denetimden (başlık sözcükleri, tam soyad belirteci, kayıt türü,
|
|
8
|
+
// verilmişse yıl) geçmezse sonuç "bulunamadi"dır.
|
|
9
|
+
//
|
|
10
|
+
// Ayrım (dış inceleme 2026-11-05): "katalogda bulundu" yalnız eserin katalogda var olduğunu söyler; "ders kaynağı
|
|
11
|
+
// olarak uygun" kararı hocanındır. Türkçe kitapların çoğu bu kataloglarda yoktur: bulunamayan eser uydurma sayılmaz
|
|
12
|
+
// ama doğrulanmış da sayılmaz; yalnız hocanın verdiği bilgiyle "hoca_beyani" etiketiyle yazılabilir.
|
|
13
|
+
//
|
|
14
|
+
// Kötüye kullanım sınırı: çağrı başına en çok 5 kaynak, süreç genelinde en çok 4 eşzamanlı dış istek, yanıt akışla
|
|
15
|
+
// okunur ve sınırda kesilir. Adres başına ayrı kota HTTP katmanındadır (http.ts).
|
|
16
|
+
//
|
|
17
|
+
// İmza: bulunan her kayıt, sürecin gizli anahtarıyla künye+bağlantı üzerinden imzalanır. Bologna araçları kaynağı
|
|
18
|
+
// "doğrulanmış" saymak için bu imzayı arar; asistan imzayı kendisi üretemez.
|
|
19
|
+
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
20
|
+
import { z } from "zod";
|
|
21
|
+
import { kunyeBicimle } from "./kaynak.js";
|
|
22
|
+
import { AracHatasi, arac, KOD_KAYNAGI } from "./temel.js";
|
|
23
|
+
export const IZINLI_HOSTLAR = ["api.crossref.org", "openlibrary.org", "www.googleapis.com"];
|
|
24
|
+
export const ZAMAN_ASIMI_MS = 8000;
|
|
25
|
+
export const EN_BUYUK_YANIT = 1_000_000;
|
|
26
|
+
/** Çağrı başına en çok kaynak. */
|
|
27
|
+
export const EN_COK_KAYNAK = 5;
|
|
28
|
+
/** Süreç genelinde aynı anda en çok dış istek. */
|
|
29
|
+
export const ESZAMANLI_DIS_ISTEK = 4;
|
|
30
|
+
// ---------------------------------------------------------------- Eşzamanlılık sınırı (semafor)
|
|
31
|
+
let aktifIstek = 0;
|
|
32
|
+
const bekleyenler = [];
|
|
33
|
+
/** Süreç genelindeki dış istek semaforu. Sınır doluysa sıra bekler. */
|
|
34
|
+
export async function sinirli(is) {
|
|
35
|
+
if (aktifIstek >= ESZAMANLI_DIS_ISTEK)
|
|
36
|
+
await new Promise((coz) => bekleyenler.push(coz));
|
|
37
|
+
aktifIstek += 1;
|
|
38
|
+
try {
|
|
39
|
+
return await is();
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
aktifIstek -= 1;
|
|
43
|
+
bekleyenler.shift()?.();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// ---------------------------------------------------------------- İmza
|
|
47
|
+
// Anahtar ilk kullanımda üretilir: modül tarayıcı araçlarına da (/deneyin) girer ve orada imza hiç kullanılmaz.
|
|
48
|
+
let imzaAnahtari;
|
|
49
|
+
function anahtar() {
|
|
50
|
+
if (!imzaAnahtari) {
|
|
51
|
+
const ortam = typeof process !== "undefined" ? process.env.AKREDIT_MCP_IMZA_ANAHTARI : undefined;
|
|
52
|
+
imzaAnahtari = ortam && ortam.length >= 16 ? Buffer.from(ortam) : randomBytes(32);
|
|
53
|
+
}
|
|
54
|
+
return imzaAnahtari;
|
|
55
|
+
}
|
|
56
|
+
/** Katalog kaydının imzası: künye ve bağlantı üzerinden HMAC-SHA256 (ilk 32 onaltılık hane). */
|
|
57
|
+
export function kaynakImzasi(kunye, baglanti) {
|
|
58
|
+
return createHmac("sha256", anahtar()).update(`${kunye}\n${baglanti}`).digest("hex").slice(0, 32);
|
|
59
|
+
}
|
|
60
|
+
/** İmza bu sürecin kaynak_dogrula çıktısına mı ait? */
|
|
61
|
+
export function kaynakImzasiDogru(kunye, baglanti, imza) {
|
|
62
|
+
const beklenen = Buffer.from(kaynakImzasi(kunye, baglanti));
|
|
63
|
+
const verilen = Buffer.from(String(imza));
|
|
64
|
+
return beklenen.length === verilen.length && timingSafeEqual(beklenen, verilen);
|
|
65
|
+
}
|
|
66
|
+
const DOI_DESENI = /^10\.\d{4,9}\/[^\s]{1,200}$/u;
|
|
67
|
+
/** "https://doi.org/10.x/y", "doi:10.x/y" ya da "10.x/y" → "10.x/y". Geçersizse null. */
|
|
68
|
+
export function doiTemizle(d) {
|
|
69
|
+
const t = d
|
|
70
|
+
.trim()
|
|
71
|
+
.replace(/^https?:\/\/(dx\.)?doi\.org\//iu, "")
|
|
72
|
+
.replace(/^doi:\s*/iu, "");
|
|
73
|
+
return DOI_DESENI.test(t) ? t : null;
|
|
74
|
+
}
|
|
75
|
+
/** ISBN-10 ya da ISBN-13; tire ve boşluk atılır, denetim basamağı sınanır. Geçersizse null. */
|
|
76
|
+
export function isbnTemizle(i) {
|
|
77
|
+
const t = i.replace(/[\s-]/gu, "").toUpperCase();
|
|
78
|
+
if (/^\d{13}$/u.test(t)) {
|
|
79
|
+
// ISBN-13: basamaklar sırayla 1 ve 3 ile çarpılır, toplam 10'un katı olmalı.
|
|
80
|
+
const top = [...t].reduce((s, c, k) => s + Number(c) * (k % 2 ? 3 : 1), 0);
|
|
81
|
+
return top % 10 === 0 ? t : null;
|
|
82
|
+
}
|
|
83
|
+
if (/^\d{9}[\dX]$/u.test(t)) {
|
|
84
|
+
// ISBN-10: basamaklar 10'dan 1'e ağırlıkla çarpılır (X = 10), toplam 11'in katı olmalı.
|
|
85
|
+
const top = [...t].reduce((s, c, k) => s + (c === "X" ? 10 : Number(c)) * (10 - k), 0);
|
|
86
|
+
return top % 11 === 0 ? t : null;
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
/** Karşılaştırma için sadeleştirme: küçük harf, Türkçe harfler düz, noktalama yok. */
|
|
91
|
+
export function sadele(s) {
|
|
92
|
+
return s
|
|
93
|
+
.toLocaleLowerCase("tr")
|
|
94
|
+
.normalize("NFD")
|
|
95
|
+
.replace(/\p{M}/gu, "")
|
|
96
|
+
.replace(/ı/gu, "i")
|
|
97
|
+
.replace(/[^\p{L}\p{N}\s]/gu, " ")
|
|
98
|
+
.split(/\s+/u)
|
|
99
|
+
.filter((x) => x.length > 1);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Sorgu başlığının sözcüklerinin en az %80'i bulunan başlıkta var mı, ve bulunanın en az %60'ı sorguda var mı.
|
|
103
|
+
* Kısa (en çok iki sözcüklü) başlıklarda ("Veri Yapıları", "Genel Kimya") sözcük kümeleri birebir aynı olmalı.
|
|
104
|
+
*/
|
|
105
|
+
export function baslikEslesir(sorgu, bulunan) {
|
|
106
|
+
const a = [...new Set(sadele(sorgu))];
|
|
107
|
+
const b = new Set(sadele(bulunan));
|
|
108
|
+
if (!a.length || !b.size)
|
|
109
|
+
return false;
|
|
110
|
+
const ortak = a.filter((x) => b.has(x)).length;
|
|
111
|
+
if (a.length <= 2)
|
|
112
|
+
return ortak === a.length && b.size === a.length;
|
|
113
|
+
return ortak / a.length >= 0.8 && ortak / b.size >= 0.6;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Sorgudaki yazar soyadı (son sözcük ya da virgülden önceki kısım) bulunan yazarlardan birinde TAM belirteç olarak
|
|
117
|
+
* geçiyor mu. Alt dize yetmez: "Yılmaz" sorgusu "Yılmazer" ile eşleşmez.
|
|
118
|
+
*/
|
|
119
|
+
export function yazarEslesir(sorgu, yazarlar) {
|
|
120
|
+
const soyad = sorgu.includes(",") ? (sorgu.split(",")[0] ?? "") : (sorgu.trim().split(/\s+/u).at(-1) ?? "");
|
|
121
|
+
const s = sadele(soyad);
|
|
122
|
+
if (!s.length)
|
|
123
|
+
return false;
|
|
124
|
+
return yazarlar.some((y) => {
|
|
125
|
+
const b = new Set(sadele(y));
|
|
126
|
+
return s.every((x) => b.has(x));
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
// ---------------------------------------------------------------- Güvenli istek
|
|
130
|
+
async function getir(f, adres) {
|
|
131
|
+
if (adres.protocol !== "https:" || !IZINLI_HOSTLAR.includes(adres.hostname))
|
|
132
|
+
throw new Error("İzinsiz host");
|
|
133
|
+
if (adres.hostname === "www.googleapis.com" && !adres.pathname.startsWith("/books/"))
|
|
134
|
+
throw new Error("İzinsiz yol");
|
|
135
|
+
return sinirli(async () => {
|
|
136
|
+
let y;
|
|
137
|
+
try {
|
|
138
|
+
y = await f(adres.toString(), {
|
|
139
|
+
signal: AbortSignal.timeout(ZAMAN_ASIMI_MS),
|
|
140
|
+
redirect: "error",
|
|
141
|
+
headers: { accept: "application/json", "user-agent": "akredit-acik-mcp (https://akredit.org)" },
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
if (!y.ok)
|
|
148
|
+
return null;
|
|
149
|
+
const uzunluk = Number(y.headers.get("content-length") ?? "0");
|
|
150
|
+
if (uzunluk > EN_BUYUK_YANIT)
|
|
151
|
+
return null;
|
|
152
|
+
const metin = await sinirliMetin(y, EN_BUYUK_YANIT);
|
|
153
|
+
if (metin === null)
|
|
154
|
+
return null;
|
|
155
|
+
try {
|
|
156
|
+
return JSON.parse(metin);
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
/** Yanıt gövdesini bayt sayarak okur; sınır aşılırsa akışı iptal edip null döner. */
|
|
164
|
+
export async function sinirliMetin(y, enBuyuk) {
|
|
165
|
+
if (!y.body)
|
|
166
|
+
return "";
|
|
167
|
+
const okuyucu = y.body.getReader();
|
|
168
|
+
const parcalar = [];
|
|
169
|
+
let toplam = 0;
|
|
170
|
+
try {
|
|
171
|
+
for (;;) {
|
|
172
|
+
const { done, value } = await okuyucu.read();
|
|
173
|
+
if (done)
|
|
174
|
+
break;
|
|
175
|
+
toplam += value.byteLength;
|
|
176
|
+
if (toplam > enBuyuk) {
|
|
177
|
+
await okuyucu.cancel().catch(() => { });
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
parcalar.push(value);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
return Buffer.concat(parcalar).toString("utf8");
|
|
187
|
+
}
|
|
188
|
+
// ---------------------------------------------------------------- Katalog yanıtları (zod)
|
|
189
|
+
const CrossrefEser = z.object({
|
|
190
|
+
DOI: z.string(),
|
|
191
|
+
type: z.string().optional(),
|
|
192
|
+
title: z.array(z.string()).optional(),
|
|
193
|
+
author: z
|
|
194
|
+
.array(z.object({ given: z.string().optional(), family: z.string().optional(), name: z.string().optional() }))
|
|
195
|
+
.optional(),
|
|
196
|
+
editor: z.array(z.object({ given: z.string().optional(), family: z.string().optional() })).optional(),
|
|
197
|
+
"container-title": z.array(z.string()).optional(),
|
|
198
|
+
publisher: z.string().optional(),
|
|
199
|
+
volume: z.string().optional(),
|
|
200
|
+
issue: z.string().optional(),
|
|
201
|
+
page: z.string().optional(),
|
|
202
|
+
issued: z.object({ "date-parts": z.array(z.array(z.number().nullable())).optional() }).optional(),
|
|
203
|
+
});
|
|
204
|
+
const CrossrefTek = z.object({ message: CrossrefEser });
|
|
205
|
+
const CrossrefListe = z.object({ message: z.object({ items: z.array(CrossrefEser) }) });
|
|
206
|
+
const OpenLibraryKitap = z.object({
|
|
207
|
+
title: z.string(),
|
|
208
|
+
url: z.string().optional(),
|
|
209
|
+
authors: z.array(z.object({ name: z.string() })).optional(),
|
|
210
|
+
publishers: z.array(z.object({ name: z.string() })).optional(),
|
|
211
|
+
publish_date: z.string().optional(),
|
|
212
|
+
});
|
|
213
|
+
const OpenLibraryYanit = z.record(z.string(), OpenLibraryKitap);
|
|
214
|
+
const GoogleCilt = z.object({
|
|
215
|
+
id: z.string(),
|
|
216
|
+
volumeInfo: z.object({
|
|
217
|
+
title: z.string(),
|
|
218
|
+
subtitle: z.string().optional(),
|
|
219
|
+
authors: z.array(z.string()).optional(),
|
|
220
|
+
publisher: z.string().optional(),
|
|
221
|
+
publishedDate: z.string().optional(),
|
|
222
|
+
industryIdentifiers: z.array(z.object({ type: z.string(), identifier: z.string() })).optional(),
|
|
223
|
+
}),
|
|
224
|
+
});
|
|
225
|
+
const GoogleYanit = z.object({ totalItems: z.number().optional(), items: z.array(GoogleCilt).optional() });
|
|
226
|
+
const HOCA_BEYANI = "Katalogda bulunamadı. Hoca bu eseri kendisi kullanıyorsa künyesini hocanın verdiği bilgiyle 'hoca_beyani' etiketiyle yazın; asistan künye alanı uydurmaz.";
|
|
227
|
+
const bulunamadi = (neden) => ({ durum: "bulunamadi", neden, hocaBeyani: HOCA_BEYANI });
|
|
228
|
+
const yilAl = (s) => {
|
|
229
|
+
const m = s?.match(/\b(1[4-9]\d{2}|20\d{2}|2100)\b/u);
|
|
230
|
+
return m ? Number(m[1]) : undefined;
|
|
231
|
+
};
|
|
232
|
+
const kur = (k, baglanti, katalog, kayitTuru = k.tur === "makale" ? "makale" : k.tur === "kitap_bolumu" ? "kitap_bolumu" : "kitap") => {
|
|
233
|
+
const g = { ...k, kurumYazar: false };
|
|
234
|
+
const { kunye, eksikler } = kunyeBicimle(g);
|
|
235
|
+
return {
|
|
236
|
+
durum: "katalogda_bulundu",
|
|
237
|
+
kunye,
|
|
238
|
+
eksikler,
|
|
239
|
+
baglanti,
|
|
240
|
+
katalog,
|
|
241
|
+
kayitTuru,
|
|
242
|
+
baslik: k.baslik,
|
|
243
|
+
yazarlar: k.yazarlar,
|
|
244
|
+
...(k.yil !== undefined ? { yil: k.yil } : {}),
|
|
245
|
+
kimlik_eslesmesi: { yontem: "baslik_yazar", denetlenen: [] },
|
|
246
|
+
bibliyografik_tamlik: eksikler.length ? "eksik" : "tam",
|
|
247
|
+
ders_kaynagi_uygunlugu: "hoca karar verir",
|
|
248
|
+
imza: kaynakImzasi(kunye, baglanti),
|
|
249
|
+
};
|
|
250
|
+
};
|
|
251
|
+
const CROSSREF_KITAP = new Set(["book", "monograph", "edited-book", "reference-book", "book-set", "book-series"]);
|
|
252
|
+
const crossrefTuru = (t) => t === "journal-article"
|
|
253
|
+
? "makale"
|
|
254
|
+
: t === "book-chapter" || t === "book-section" || t === "book-part"
|
|
255
|
+
? "kitap_bolumu"
|
|
256
|
+
: t && CROSSREF_KITAP.has(t)
|
|
257
|
+
? "kitap"
|
|
258
|
+
: "diger";
|
|
259
|
+
function crossrefKunye(e) {
|
|
260
|
+
const baslik = e.title?.[0]?.trim();
|
|
261
|
+
if (!baslik)
|
|
262
|
+
return null;
|
|
263
|
+
const yazarlar = (e.author ?? [])
|
|
264
|
+
.map((a) => (a.family ? [a.family, a.given].filter(Boolean).join(", ") : (a.name ?? "")))
|
|
265
|
+
.filter(Boolean);
|
|
266
|
+
const yil = e.issued?.["date-parts"]?.[0]?.[0] ?? undefined;
|
|
267
|
+
const kap = e["container-title"]?.[0];
|
|
268
|
+
const kayit = crossrefTuru(e.type);
|
|
269
|
+
const tur = kayit === "makale" ? "makale" : kayit === "kitap_bolumu" ? "kitap_bolumu" : "kitap";
|
|
270
|
+
const doi = e.DOI;
|
|
271
|
+
return kur({
|
|
272
|
+
tur,
|
|
273
|
+
yazarlar,
|
|
274
|
+
baslik,
|
|
275
|
+
...(yil ? { yil } : {}),
|
|
276
|
+
...(tur === "makale" && kap ? { dergi: kap } : {}),
|
|
277
|
+
...(tur === "kitap_bolumu" && kap ? { kitapBasligi: kap } : {}),
|
|
278
|
+
...(tur !== "makale" && e.publisher ? { yayinevi: e.publisher } : {}),
|
|
279
|
+
...(tur === "kitap_bolumu" && e.editor?.length
|
|
280
|
+
? {
|
|
281
|
+
editorler: e.editor.map((x) => [x.family, x.given].filter(Boolean).join(", ")).filter((x) => x.length >= 2),
|
|
282
|
+
}
|
|
283
|
+
: {}),
|
|
284
|
+
...(e.volume ? { cilt: e.volume } : {}),
|
|
285
|
+
...(e.issue ? { sayi: e.issue } : {}),
|
|
286
|
+
...(e.page ? { sayfalar: e.page } : {}),
|
|
287
|
+
doi,
|
|
288
|
+
}, `https://doi.org/${doi}`, "Crossref", kayit);
|
|
289
|
+
}
|
|
290
|
+
function googleKunye(c) {
|
|
291
|
+
const v = c.volumeInfo;
|
|
292
|
+
const yil = yilAl(v.publishedDate);
|
|
293
|
+
return kur({
|
|
294
|
+
tur: "kitap",
|
|
295
|
+
yazarlar: v.authors ?? [],
|
|
296
|
+
baslik: v.subtitle ? `${v.title}: ${v.subtitle}` : v.title,
|
|
297
|
+
...(yil ? { yil } : {}),
|
|
298
|
+
...(v.publisher ? { yayinevi: v.publisher } : {}),
|
|
299
|
+
}, `https://books.google.com/books?id=${encodeURIComponent(c.id)}`, "Google Books");
|
|
300
|
+
}
|
|
301
|
+
// ---------------------------------------------------------------- Sorgular
|
|
302
|
+
const CROSSREF = "https://api.crossref.org/works";
|
|
303
|
+
const OPENLIBRARY = "https://openlibrary.org/api/books";
|
|
304
|
+
const GOOGLE = "https://www.googleapis.com/books/v1/volumes";
|
|
305
|
+
const KAYIT_ADI = {
|
|
306
|
+
kitap: "kitap",
|
|
307
|
+
makale: "dergi makalesi",
|
|
308
|
+
kitap_bolumu: "kitap bölümü",
|
|
309
|
+
diger: "başka türde bir kayıt (bildiri, rapor...)",
|
|
310
|
+
};
|
|
311
|
+
/** Aranan tür ve yıl verildiyse kayıtla uyuşuyor mu; uyuşmuyorsa nedeni. */
|
|
312
|
+
function ekDenetim(k, s) {
|
|
313
|
+
if (s.tur && s.tur !== "herhangi" && k.kayitTuru !== s.tur)
|
|
314
|
+
return `Katalogdaki kayıt ${KAYIT_ADI[k.kayitTuru]}; aranan ${KAYIT_ADI[s.tur]}. Aynı eser sayılmadı.`;
|
|
315
|
+
if (s.yil !== undefined) {
|
|
316
|
+
const y = typeof k.yil === "number" ? k.yil : yilAl(String(k.yil ?? ""));
|
|
317
|
+
if (y === undefined)
|
|
318
|
+
return "Katalog kaydında yıl yok; verilen yılla karşılaştırılamadı.";
|
|
319
|
+
if (y !== s.yil)
|
|
320
|
+
return `Katalogdaki yıl ${y}, verilen yıl ${s.yil}. Aynı baskı sayılmadı.`;
|
|
321
|
+
}
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
function sonla(k, yontem, s, bulunamadiNedeni) {
|
|
325
|
+
if (!k)
|
|
326
|
+
return bulunamadi(bulunamadiNedeni);
|
|
327
|
+
const sorun = ekDenetim(k, s);
|
|
328
|
+
if (sorun)
|
|
329
|
+
return bulunamadi(sorun);
|
|
330
|
+
const denetlenen = [
|
|
331
|
+
yontem === "doi" ? "DOI" : yontem === "isbn" ? "ISBN (sağlama basamağı geçerli)" : "başlık sözcükleri, tam soyad",
|
|
332
|
+
...(s.tur && s.tur !== "herhangi" ? ["kayıt türü"] : []),
|
|
333
|
+
...(s.yil !== undefined ? ["yıl"] : []),
|
|
334
|
+
];
|
|
335
|
+
return { ...k, kimlik_eslesmesi: { yontem, denetlenen } };
|
|
336
|
+
}
|
|
337
|
+
async function doiIle(f, doi) {
|
|
338
|
+
const y = CrossrefTek.safeParse(await getir(f, new URL(`${CROSSREF}/${encodeURIComponent(doi)}`)));
|
|
339
|
+
return y.success ? crossrefKunye(y.data.message) : null;
|
|
340
|
+
}
|
|
341
|
+
async function isbnIle(f, isbn) {
|
|
342
|
+
const ol = new URL(OPENLIBRARY);
|
|
343
|
+
ol.searchParams.set("bibkeys", `ISBN:${isbn}`);
|
|
344
|
+
ol.searchParams.set("format", "json");
|
|
345
|
+
ol.searchParams.set("jscmd", "data");
|
|
346
|
+
const o = OpenLibraryYanit.safeParse(await getir(f, ol));
|
|
347
|
+
const kitap = o.success ? o.data[`ISBN:${isbn}`] : undefined;
|
|
348
|
+
if (kitap) {
|
|
349
|
+
const yil = yilAl(kitap.publish_date);
|
|
350
|
+
const baglanti = kitap.url && /^https:\/\/openlibrary\.org\//u.test(kitap.url)
|
|
351
|
+
? kitap.url
|
|
352
|
+
: `https://openlibrary.org/isbn/${isbn}`;
|
|
353
|
+
return kur({
|
|
354
|
+
tur: "kitap",
|
|
355
|
+
yazarlar: (kitap.authors ?? []).map((a) => a.name),
|
|
356
|
+
baslik: kitap.title,
|
|
357
|
+
...(yil ? { yil } : {}),
|
|
358
|
+
...(kitap.publishers?.[0] ? { yayinevi: kitap.publishers[0].name } : {}),
|
|
359
|
+
}, baglanti, "Open Library");
|
|
360
|
+
}
|
|
361
|
+
const g = new URL(GOOGLE);
|
|
362
|
+
g.searchParams.set("q", `isbn:${isbn}`);
|
|
363
|
+
const gy = GoogleYanit.safeParse(await getir(f, g));
|
|
364
|
+
const c = gy.success
|
|
365
|
+
? gy.data.items?.find((x) => (x.volumeInfo.industryIdentifiers ?? []).some((i) => i.identifier.replace(/-/gu, "") === isbn))
|
|
366
|
+
: undefined;
|
|
367
|
+
return c ? googleKunye(c) : null;
|
|
368
|
+
}
|
|
369
|
+
async function baslikIle(f, baslik, yazar, s) {
|
|
370
|
+
const uygun = (k) => baslikEslesir(baslik, k.baslik) && yazarEslesir(yazar, k.yazarlar) && ekDenetim(k, s) === null;
|
|
371
|
+
const g = new URL(GOOGLE);
|
|
372
|
+
g.searchParams.set("q", `intitle:${baslik} inauthor:${yazar}`);
|
|
373
|
+
g.searchParams.set("maxResults", "5");
|
|
374
|
+
const gy = GoogleYanit.safeParse(await getir(f, g));
|
|
375
|
+
if (gy.success)
|
|
376
|
+
for (const x of gy.data.items ?? []) {
|
|
377
|
+
const k = googleKunye(x);
|
|
378
|
+
if (baslikEslesir(baslik, x.volumeInfo.title) && uygun(k))
|
|
379
|
+
return k;
|
|
380
|
+
}
|
|
381
|
+
const cr = new URL(CROSSREF);
|
|
382
|
+
cr.searchParams.set("query.bibliographic", baslik);
|
|
383
|
+
cr.searchParams.set("query.author", yazar);
|
|
384
|
+
cr.searchParams.set("rows", "5");
|
|
385
|
+
const cy = CrossrefListe.safeParse(await getir(f, cr));
|
|
386
|
+
if (cy.success)
|
|
387
|
+
for (const e of cy.data.message.items) {
|
|
388
|
+
const k = crossrefKunye(e);
|
|
389
|
+
if (k && uygun(k))
|
|
390
|
+
return k;
|
|
391
|
+
}
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
const KaynakSorgusu = z
|
|
395
|
+
.object({
|
|
396
|
+
doi: z.string().trim().max(250).optional(),
|
|
397
|
+
isbn: z.string().trim().max(30).optional(),
|
|
398
|
+
baslik: z.string().trim().min(2).max(300).optional(),
|
|
399
|
+
yazar: z.string().trim().min(2).max(120).optional(),
|
|
400
|
+
tur: z
|
|
401
|
+
.enum(["kitap", "makale", "kitap_bolumu", "herhangi"])
|
|
402
|
+
.optional()
|
|
403
|
+
.describe("Aranan kayıt türü. Kitap aranıyorsa dergi makalesi kabul edilmez. ISBN verilirse varsayılan kitap."),
|
|
404
|
+
yil: z.number().int().min(1400).max(2100).optional().describe("Biliniyorsa yayın yılı; kayıtla birebir uymalı"),
|
|
405
|
+
})
|
|
406
|
+
.refine((k) => k.doi || k.isbn || (k.baslik && k.yazar), {
|
|
407
|
+
message: "DOI, ISBN ya da başlık ile yazar verin.",
|
|
408
|
+
});
|
|
409
|
+
/** Tek kaynağı doğrular. DOI önce, sonra ISBN, sonra başlık+yazar. */
|
|
410
|
+
export async function kaynakDogrula(f, k) {
|
|
411
|
+
const ek = { tur: k.tur, yil: k.yil };
|
|
412
|
+
if (k.doi) {
|
|
413
|
+
const d = doiTemizle(k.doi);
|
|
414
|
+
if (!d)
|
|
415
|
+
return bulunamadi("DOI biçimi geçersiz (10.xxxx/... olmalı).");
|
|
416
|
+
return sonla(await doiIle(f, d), "doi", ek, "Bu DOI Crossref kataloğunda bulunamadı.");
|
|
417
|
+
}
|
|
418
|
+
if (k.isbn) {
|
|
419
|
+
const i = isbnTemizle(k.isbn);
|
|
420
|
+
if (!i)
|
|
421
|
+
return bulunamadi("ISBN geçersiz (sağlama basamağı tutmuyor).");
|
|
422
|
+
return sonla(await isbnIle(f, i), "isbn", { ...ek, tur: k.tur ?? "kitap" }, "Bu ISBN Open Library ve Google Books'ta bulunamadı.");
|
|
423
|
+
}
|
|
424
|
+
return sonla(await baslikIle(f, k.baslik ?? "", k.yazar ?? "", ek), "baslik_yazar", ek, "Bu başlık ve yazarla (tam soyad, verildiyse tür ve yıl) eşleşen eser Google Books ve Crossref kataloglarında bulunamadı. Türkçe kitapların çoğu bu kataloglarda yoktur.");
|
|
425
|
+
}
|
|
426
|
+
const KATALOG_KAYNAKLARI = [
|
|
427
|
+
{ ad: "Crossref REST API", adres: "https://api.crossref.org" },
|
|
428
|
+
{ ad: "Open Library Books API", adres: "https://openlibrary.org/developers/api" },
|
|
429
|
+
{ ad: "Google Books API", adres: "https://developers.google.com/books" },
|
|
430
|
+
];
|
|
431
|
+
export const kaynakDogrulaAraci = arac({
|
|
432
|
+
ad: "kaynak_dogrula",
|
|
433
|
+
baslik: "Kaynağın gerçekten var olduğunu doğrula",
|
|
434
|
+
aciklama: "DOI, ISBN ya da başlık ile yazar verilen eserleri (çağrı başına en çok 5) açık kataloglarda (Crossref, Open Library, Google Books) arar. Eşleşme için tam soyad, başlık sözcükleri, verildiyse kayıt türü ve yıl denetlenir. Bulursa katalogdaki bilgiden künye, bağlantı ve imza döndürür ('katalogda_bulundu'); bulamazsa 'bulunamadi' der. Katalogda bulunmak ders kaynağı olarak uygunluk demek değildir; ona hoca karar verir.",
|
|
435
|
+
sonrakiAdim: "Yalnız 'katalogda_bulundu' olan kaynakları hocaya künyesi ve bağlantısıyla gösterin; uygunluğuna hoca karar verir. Bologna araçlarına kaynağı { kunye, dogrulama: { durum, baglanti, imza } } olarak aynen verin. 'bulunamadi' olan kaynağı asla kendiniz önermeyin; hoca o eseri kullandığını söylerse yalnız hocanın verdiği bilgiyle dogrulama: 'hoca_beyani' olarak yazın.",
|
|
436
|
+
kunyeAlanlari: ["kaynaklar"],
|
|
437
|
+
disAg: true,
|
|
438
|
+
// Künye ve katalog numaraları kişisel veri değildir; ISBN gibi uzun sayılar numara deseniyle karışmasın.
|
|
439
|
+
kisiselVeriKapsami: "yok",
|
|
440
|
+
girdi: z.object({
|
|
441
|
+
kaynaklar: z.array(KaynakSorgusu).min(1).max(EN_COK_KAYNAK),
|
|
442
|
+
}),
|
|
443
|
+
calistir: async ({ kaynaklar }, b) => {
|
|
444
|
+
const f = b.fetch ?? globalThis.fetch;
|
|
445
|
+
if (!f)
|
|
446
|
+
throw new AracHatasi("Bu ortamda dış katalog sorgusu yapılamıyor. Kaynakları şimdilik önermeyin.");
|
|
447
|
+
const sonuclar = await Promise.all(kaynaklar.map((k) => kaynakDogrula(f, k)));
|
|
448
|
+
const liste = sonuclar.map((s, i) => ({ no: i + 1, ...s }));
|
|
449
|
+
const bulunan = liste.filter((s) => s.durum === "katalogda_bulundu").length;
|
|
450
|
+
return {
|
|
451
|
+
sonuc: {
|
|
452
|
+
kaynaklar: liste,
|
|
453
|
+
katalogdaBulunan: bulunan,
|
|
454
|
+
bulunamayan: liste.length - bulunan,
|
|
455
|
+
kural: "Katalogda bulunmayan kaynak asistanca önerilmez; hoca kullandığını söylerse 'hoca_beyani' etiketiyle yazılır. Künye katalog bilgisinden kurulur; katalogda olmayan alan [EKSİK] kalır. Ders kaynağı olarak uygunluğa hoca karar verir.",
|
|
456
|
+
},
|
|
457
|
+
kaynak: [KOD_KAYNAGI("apps/acik-mcp/src/kaynak-dogrula.ts", "Akredit kaynak doğrulayıcı"), ...KATALOG_KAYNAKLARI],
|
|
458
|
+
};
|
|
459
|
+
},
|
|
460
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
declare const Kaynak: z.ZodObject<{
|
|
3
|
+
tur: z.ZodDefault<z.ZodEnum<{
|
|
4
|
+
web: "web";
|
|
5
|
+
kitap: "kitap";
|
|
6
|
+
makale: "makale";
|
|
7
|
+
kitap_bolumu: "kitap_bolumu";
|
|
8
|
+
tez: "tez";
|
|
9
|
+
}>>;
|
|
10
|
+
yazarlar: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
11
|
+
kurumYazar: z.ZodDefault<z.ZodBoolean>;
|
|
12
|
+
yil: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>>;
|
|
13
|
+
baslik: z.ZodString;
|
|
14
|
+
baski: z.ZodOptional<z.ZodNumber>;
|
|
15
|
+
yayinevi: z.ZodOptional<z.ZodString>;
|
|
16
|
+
dergi: z.ZodOptional<z.ZodString>;
|
|
17
|
+
cilt: z.ZodOptional<z.ZodString>;
|
|
18
|
+
sayi: z.ZodOptional<z.ZodString>;
|
|
19
|
+
sayfalar: z.ZodOptional<z.ZodString>;
|
|
20
|
+
editorler: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
21
|
+
kitapBasligi: z.ZodOptional<z.ZodString>;
|
|
22
|
+
kurum: z.ZodOptional<z.ZodString>;
|
|
23
|
+
tezTuru: z.ZodOptional<z.ZodEnum<{
|
|
24
|
+
yuksek_lisans: "yuksek_lisans";
|
|
25
|
+
doktora: "doktora";
|
|
26
|
+
}>>;
|
|
27
|
+
site: z.ZodOptional<z.ZodString>;
|
|
28
|
+
doi: z.ZodOptional<z.ZodString>;
|
|
29
|
+
url: z.ZodOptional<z.ZodString>;
|
|
30
|
+
}, z.core.$strip>;
|
|
31
|
+
type KaynakT = z.infer<typeof Kaynak>;
|
|
32
|
+
/** "Soyad, Ad İkinci" ya da "Ad İkinci Soyad" → "Soyad, A. İ." */
|
|
33
|
+
export declare function yazarBicimle(ad: string, kurum?: boolean): string;
|
|
34
|
+
/** APA 7: 1 yazar "A"; 2 yazar "A, & B"; 3-20 yazar virgülle, sonuncudan önce "&"; 21+ ilk 19, "...", son. */
|
|
35
|
+
export declare function yazarListesi(yazarlar: string[], kurum?: boolean): string;
|
|
36
|
+
/** Tek künye. Eksik zorunlu bilgi [EKSİK: ...] olarak yazılır ve ayrıca listelenir. */
|
|
37
|
+
export declare function kunyeBicimle(k: KaynakT): {
|
|
38
|
+
kunye: string;
|
|
39
|
+
eksikler: string[];
|
|
40
|
+
};
|
|
41
|
+
export declare const kaynakBicimleAraci: import("./temel.ts").AcikArac<z.ZodObject<{
|
|
42
|
+
kaynaklar: z.ZodArray<z.ZodObject<{
|
|
43
|
+
tur: z.ZodDefault<z.ZodEnum<{
|
|
44
|
+
web: "web";
|
|
45
|
+
kitap: "kitap";
|
|
46
|
+
makale: "makale";
|
|
47
|
+
kitap_bolumu: "kitap_bolumu";
|
|
48
|
+
tez: "tez";
|
|
49
|
+
}>>;
|
|
50
|
+
yazarlar: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
51
|
+
kurumYazar: z.ZodDefault<z.ZodBoolean>;
|
|
52
|
+
yil: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>>;
|
|
53
|
+
baslik: z.ZodString;
|
|
54
|
+
baski: z.ZodOptional<z.ZodNumber>;
|
|
55
|
+
yayinevi: z.ZodOptional<z.ZodString>;
|
|
56
|
+
dergi: z.ZodOptional<z.ZodString>;
|
|
57
|
+
cilt: z.ZodOptional<z.ZodString>;
|
|
58
|
+
sayi: z.ZodOptional<z.ZodString>;
|
|
59
|
+
sayfalar: z.ZodOptional<z.ZodString>;
|
|
60
|
+
editorler: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
61
|
+
kitapBasligi: z.ZodOptional<z.ZodString>;
|
|
62
|
+
kurum: z.ZodOptional<z.ZodString>;
|
|
63
|
+
tezTuru: z.ZodOptional<z.ZodEnum<{
|
|
64
|
+
yuksek_lisans: "yuksek_lisans";
|
|
65
|
+
doktora: "doktora";
|
|
66
|
+
}>>;
|
|
67
|
+
site: z.ZodOptional<z.ZodString>;
|
|
68
|
+
doi: z.ZodOptional<z.ZodString>;
|
|
69
|
+
url: z.ZodOptional<z.ZodString>;
|
|
70
|
+
}, z.core.$strip>>;
|
|
71
|
+
}, z.core.$strip>>;
|
|
72
|
+
export {};
|