@akredit/acik-mcp 0.4.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 +5 -5
- package/dist/apps/acik-mcp/src/araclar.js +2 -0
- package/dist/apps/acik-mcp/src/bologna.d.ts +91 -9
- package/dist/apps/acik-mcp/src/bologna.js +477 -105
- package/dist/apps/acik-mcp/src/genel-ayristir.d.ts +10 -1
- package/dist/apps/acik-mcp/src/genel-ayristir.js +85 -7
- package/dist/apps/acik-mcp/src/http.d.ts +31 -3
- package/dist/apps/acik-mcp/src/http.js +86 -15
- package/dist/apps/acik-mcp/src/index.d.ts +1 -0
- package/dist/apps/acik-mcp/src/index.js +1 -0
- package/dist/apps/acik-mcp/src/kaynak-dogrula.d.ts +51 -4
- package/dist/apps/acik-mcp/src/kaynak-dogrula.js +199 -58
- package/dist/apps/acik-mcp/src/kisisel-veri.d.ts +3 -1
- package/dist/apps/acik-mcp/src/kisisel-veri.js +24 -9
- package/dist/apps/acik-mcp/src/not-tablosu.d.ts +6 -0
- package/dist/apps/acik-mcp/src/not-tablosu.js +40 -15
- package/dist/apps/acik-mcp/src/promptlar.d.ts +23 -0
- package/dist/apps/acik-mcp/src/promptlar.js +98 -28
- package/dist/apps/acik-mcp/src/sunucu.d.ts +1 -1
- package/dist/apps/acik-mcp/src/sunucu.js +10 -5
- package/dist/apps/acik-mcp/src/temel.d.ts +5 -0
- package/dist/apps/acik-mcp/src/yardim.js +28 -18
- 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 +2 -2
|
@@ -1,9 +1,18 @@
|
|
|
1
|
-
export type GenelAlan = "kod" | "ad" | "yariyil" | "tul" | "kredi" | "akts" | "dil" | "tur" | "onKosul" | "amac" | "icerik" | "yontem" | "notlar" | "ciktilar" | "haftalar" | "degerlendirme" | "isyuku" | "kaynaklar" | "kisi";
|
|
1
|
+
export type GenelAlan = "kod" | "ad" | "yariyil" | "tul" | "kredi" | "akts" | "dil" | "tur" | "onKosul" | "amac" | "icerik" | "yontem" | "notlar" | "ciktilar" | "haftalar" | "degerlendirme" | "isyuku" | "kaynaklar" | "katki" | "dersYapisi" | "kisi";
|
|
2
2
|
/** Bir hücre ya da satır başı bir etiket mi? Tam eşleşme (sondaki iki nokta ve parantez yok sayılır). */
|
|
3
3
|
export declare function etiketAlani(s: string): GenelAlan | null;
|
|
4
4
|
export type Satir = string[];
|
|
5
5
|
/** HTML ya da düz metni satır ve hücrelere çevirir. Tablo hücreleri sekmeyle ayrılır. */
|
|
6
6
|
export declare function satirlara(girdi: string): Satir[];
|
|
7
|
+
/**
|
|
8
|
+
* Sekmeli satırlardan ÖÇ-PÇ katkı matrisi: başlıkta en az iki P kodu, altında Ö satırları. Değerler "1"-"5" ya da
|
|
9
|
+
* boş. Başlıkta satır etiketi sütunu yoksa veri satırları bir sütun kaydırılır. Bulunamazsa boş nesne.
|
|
10
|
+
*/
|
|
11
|
+
export declare function katkiMatrisi(satirlar: Satir[]): Record<string, Record<string, string>>;
|
|
12
|
+
/** Sayfada İngilizce alan var mı (İngilizce etiket ya da "(İngilizce)" alanı). Bologna araçlarının ingilizce değeri. */
|
|
13
|
+
export declare function ingilizceAlanVar(girdi: string): boolean;
|
|
14
|
+
/** Proliz ders sayfasının düz metni mi (Ctrl+A, Ctrl+C)? */
|
|
15
|
+
export declare function prolizMetniMi(girdi: string): boolean;
|
|
7
16
|
export interface Bolum {
|
|
8
17
|
alan: GenelAlan;
|
|
9
18
|
/** Sayfadaki etiket, olduğu gibi (ör. "Dersi Verenler"). */
|
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
//
|
|
5
5
|
// Kişi satırları (Dersi Veren, Instructor, e-posta, telefon) ayrıştırmadan önce bellekte atılır. Kaynaklar
|
|
6
6
|
// bölümü künye olarak ayrı alınır (yazar adları yayımlanmış künyedir).
|
|
7
|
+
//
|
|
8
|
+
// Düz metin (dış inceleme 2026-11-05): tarayıcıda Ctrl+A, Ctrl+C ile kopyalanan sayfada tablolar sekmeyle ayrılır.
|
|
9
|
+
// ÖÇ-PÇ katkı matrisi ve ders yapısı bu sekmeli metinden de okunur; Proliz sayfasının düz metni tanınır.
|
|
7
10
|
import { metin as htmlMetni, sayi } from "../../../packages/baglayicilar/src/bologna-proliz/ayristir.js";
|
|
8
11
|
/** Etiketler küçük harfle, Türkçe karakterle yazılır. Uzun etiket önce denenir. */
|
|
9
12
|
const ETIKETLER = {
|
|
@@ -101,6 +104,19 @@ const ETIKETLER = {
|
|
|
101
104
|
"recommended sources",
|
|
102
105
|
"reading list",
|
|
103
106
|
],
|
|
107
|
+
katki: [
|
|
108
|
+
"program çıktısına katkı",
|
|
109
|
+
"program çıktısına katkısı",
|
|
110
|
+
"prg.çıktısına katkısı",
|
|
111
|
+
"prg. çıktısına katkısı",
|
|
112
|
+
"program çıktılarına katkı",
|
|
113
|
+
"dersin program çıktılarına katkısı",
|
|
114
|
+
"öğrenme çıktılarının program çıktılarına katkısı",
|
|
115
|
+
"ders öğrenme çıktıları ve program çıktıları ilişkisi",
|
|
116
|
+
"contribution to program outcomes",
|
|
117
|
+
"contribution of learning outcomes to programme outcomes",
|
|
118
|
+
],
|
|
119
|
+
dersYapisi: ["ders yapısı", "dersin yapısı", "ders kategorisi", "course structure", "course category"],
|
|
104
120
|
kisi: [
|
|
105
121
|
"dersi veren",
|
|
106
122
|
"dersi verenler",
|
|
@@ -157,6 +173,19 @@ function onekEtiketi(s) {
|
|
|
157
173
|
const alan = etiketAlani(m[1]);
|
|
158
174
|
return alan ? { alan, kalan: m[2] ?? "" } : null;
|
|
159
175
|
}
|
|
176
|
+
/** Program çıktısı kodu (P1, PÇ2, PO3) ve ders çıktısı kodu (Ö1, ÖÇ2, LO3) ya da "Tüm" satırı. */
|
|
177
|
+
const P_KODU = /^(?:P|PÇ|PC|PO|PY)\s*-?\s*(\d{1,2})$/iu;
|
|
178
|
+
const O_KODU = /^(?:Ö|ÖÇ|DÖÇ|LO|CLO|ÇK)\s*-?\s*(\d{1,2})$|^(Tüm|All)$/iu;
|
|
179
|
+
/** Katkı matrisinin başlık ya da değer satırı mı? Böyle satırlarda boş hücre "-" olarak korunur (sütun hizası). */
|
|
180
|
+
function matrisSatiri(h) {
|
|
181
|
+
if (h.filter((x) => P_KODU.test(x)).length >= 2)
|
|
182
|
+
return true;
|
|
183
|
+
const ilk = h.findIndex((x) => x !== "");
|
|
184
|
+
return (ilk >= 0 &&
|
|
185
|
+
O_KODU.test(h[ilk] ?? "") &&
|
|
186
|
+
h.length - ilk >= 3 &&
|
|
187
|
+
h.slice(ilk + 1).every((x) => x === "" || /^[1-5-]$/u.test(x)));
|
|
188
|
+
}
|
|
160
189
|
/** HTML ya da düz metni satır ve hücrelere çevirir. Tablo hücreleri sekmeyle ayrılır. */
|
|
161
190
|
export function satirlara(girdi) {
|
|
162
191
|
const htmlMi = /<\/?(table|tr|td|th|p|div|br|li|h\d|body|span)\b/iu.test(girdi);
|
|
@@ -174,11 +203,51 @@ export function satirlara(girdi) {
|
|
|
174
203
|
: girdi;
|
|
175
204
|
return duz
|
|
176
205
|
.split(/\r?\n/u)
|
|
177
|
-
.map((s) =>
|
|
178
|
-
.split("\t")
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
.filter((r) => r.
|
|
206
|
+
.map((s) => {
|
|
207
|
+
const h = s.split("\t").map((x) => x.replace(/\s+/gu, " ").trim());
|
|
208
|
+
return matrisSatiri(h) ? h.map((x) => x || "-") : h.filter(Boolean);
|
|
209
|
+
})
|
|
210
|
+
.filter((r) => r.some((x) => x && x !== "-"));
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Sekmeli satırlardan ÖÇ-PÇ katkı matrisi: başlıkta en az iki P kodu, altında Ö satırları. Değerler "1"-"5" ya da
|
|
214
|
+
* boş. Başlıkta satır etiketi sütunu yoksa veri satırları bir sütun kaydırılır. Bulunamazsa boş nesne.
|
|
215
|
+
*/
|
|
216
|
+
export function katkiMatrisi(satirlar) {
|
|
217
|
+
for (let i = 0; i < satirlar.length; i++) {
|
|
218
|
+
const baslik = satirlar[i] ?? [];
|
|
219
|
+
const sutunlar = baslik
|
|
220
|
+
.map((h, j) => ({ j, m: h.match(P_KODU) }))
|
|
221
|
+
.filter((x) => x.m !== null);
|
|
222
|
+
if (sutunlar.length < 2)
|
|
223
|
+
continue;
|
|
224
|
+
const kayma = P_KODU.test(baslik[0] ?? "") ? 1 : 0;
|
|
225
|
+
const sonuc = {};
|
|
226
|
+
for (let k = i + 1; k < satirlar.length; k++) {
|
|
227
|
+
const r = satirlar[k] ?? [];
|
|
228
|
+
const m = (r[0] ?? "").match(O_KODU);
|
|
229
|
+
if (!m)
|
|
230
|
+
break;
|
|
231
|
+
const anahtar = m[2] ? "Tüm" : `Ö${Number(m[1])}`;
|
|
232
|
+
sonuc[anahtar] = Object.fromEntries(sutunlar.map(({ j, m: pm }) => {
|
|
233
|
+
const v = (r[j + kayma] ?? "").trim();
|
|
234
|
+
return [`P${Number(pm[1])}`, /^[1-5]$/u.test(v) ? v : ""];
|
|
235
|
+
}));
|
|
236
|
+
}
|
|
237
|
+
if (Object.keys(sonuc).length)
|
|
238
|
+
return sonuc;
|
|
239
|
+
}
|
|
240
|
+
return {};
|
|
241
|
+
}
|
|
242
|
+
const INGILIZCE_ALAN = /\b(course objectives?|learning outcomes|course contents?|weekly (topics|schedule|subjects)|ects workload|assessment methods?|teaching methods|course description)\b|\(\s*([iİ]ngilizce|english)\s*\)|[iİ]ngilizce (amaç|içerik|adı)/iu;
|
|
243
|
+
/** Sayfada İngilizce alan var mı (İngilizce etiket ya da "(İngilizce)" alanı). Bologna araçlarının ingilizce değeri. */
|
|
244
|
+
export function ingilizceAlanVar(girdi) {
|
|
245
|
+
return INGILIZCE_ALAN.test(girdi.slice(0, 400_000));
|
|
246
|
+
}
|
|
247
|
+
const PROLIZ_METNI = /Yarıyıl\s*\t\s*Kodu|Prg\.\s*Çıktısına\s*Katkısı|Sür\.\s*Kalkınma\s*Amaçları|Semester\s*\t\s*Code/iu;
|
|
248
|
+
/** Proliz ders sayfasının düz metni mi (Ctrl+A, Ctrl+C)? */
|
|
249
|
+
export function prolizMetniMi(girdi) {
|
|
250
|
+
return !/<\/?(table|tr|td)\b/iu.test(girdi) && PROLIZ_METNI.test(girdi);
|
|
182
251
|
}
|
|
183
252
|
/**
|
|
184
253
|
* Satırları etiketli bölümlere ayırır. Başlık satırı (bütün hücreleri etiket olan) bir sonraki satırla
|
|
@@ -277,6 +346,8 @@ const ALAN_ADI = {
|
|
|
277
346
|
degerlendirme: "Değerlendirme",
|
|
278
347
|
isyuku: "AKTS iş yükü",
|
|
279
348
|
kaynaklar: "Kaynaklar",
|
|
349
|
+
katki: "Program çıktısına katkı",
|
|
350
|
+
dersYapisi: "Ders yapısı",
|
|
280
351
|
};
|
|
281
352
|
export const GENEL_ALAN_ADLARI = ALAN_ADI;
|
|
282
353
|
const sayilar = (s) => [...s.replace(/(\d),(\d)/gu, "$1.$2").matchAll(/(?<![\p{L}\d])%?\s*(\d+(?:\.\d+)?)/gu)].map((m) => Number(m[1]));
|
|
@@ -360,6 +431,11 @@ export function genelAyristir(girdi, kaynakSayisi = 0) {
|
|
|
360
431
|
toplam: n.length >= 3 ? (n[2] ?? null) : s !== null && sure !== null ? s * sure : null,
|
|
361
432
|
};
|
|
362
433
|
});
|
|
434
|
+
const katki = katkiMatrisi(satirlar);
|
|
435
|
+
const dersYapisi = satirTablosu("dersYapisi")
|
|
436
|
+
.map(adVeSayilar)
|
|
437
|
+
.filter((x) => x.ad && /\p{L}/u.test(x.ad) && x.n.length >= 1 && !TOPLAM.test(x.ad))
|
|
438
|
+
.map(({ ad, n }) => ({ alan: ad, yuzde: n.at(-1) ?? null }));
|
|
363
439
|
const bilgiler = {
|
|
364
440
|
...(tekDeger("dil") ? { dil: tekDeger("dil") } : {}),
|
|
365
441
|
...(tekDeger("tur") ? { tur: tekDeger("tur") } : {}),
|
|
@@ -379,10 +455,10 @@ export function genelAyristir(girdi, kaynakSayisi = 0) {
|
|
|
379
455
|
ciktilar,
|
|
380
456
|
degerlendirme,
|
|
381
457
|
isyuku,
|
|
382
|
-
dersYapisi
|
|
458
|
+
dersYapisi,
|
|
383
459
|
haftalar: haftaKonulari.length,
|
|
384
460
|
haftaKonulari,
|
|
385
|
-
katki
|
|
461
|
+
katki,
|
|
386
462
|
};
|
|
387
463
|
const varMi = {
|
|
388
464
|
kod: Boolean(ders.kod),
|
|
@@ -402,6 +478,8 @@ export function genelAyristir(girdi, kaynakSayisi = 0) {
|
|
|
402
478
|
degerlendirme: degerlendirme.length > 0,
|
|
403
479
|
isyuku: isyuku.length > 0,
|
|
404
480
|
kaynaklar: kaynakSayisi > 0,
|
|
481
|
+
katki: Object.keys(katki).length > 0,
|
|
482
|
+
dersYapisi: dersYapisi.length > 0,
|
|
405
483
|
};
|
|
406
484
|
const bulunan = Object.keys(varMi).filter((k) => varMi[k]);
|
|
407
485
|
const bulunamayan = Object.keys(varMi).filter((k) => !varMi[k]);
|
|
@@ -17,11 +17,29 @@ export interface AcikHttpSecenekleri {
|
|
|
17
17
|
};
|
|
18
18
|
/** Günlük yazıcı. Varsayılan: stdout'a tek satır JSON. */
|
|
19
19
|
gunluk?: (k: GunlukKaydi) => void;
|
|
20
|
-
/**
|
|
21
|
-
|
|
20
|
+
/**
|
|
21
|
+
* x-forwarded-for listesinde sondan kaç öğenin güvenilen vekillerce yazıldığı. Varsayılan 1: istemci adresi
|
|
22
|
+
* listenin son öğesidir (vekil gerçek adresi sona ekler; baştaki öğeler istemcinin kendi yazdığıdır, sahtelenebilir).
|
|
23
|
+
*/
|
|
24
|
+
guvenilenVekil?: number;
|
|
25
|
+
/** kaynak_dogrula için adres başına ayrı kova (dış katalog sorgusu pahalıdır). Varsayılan dakikada 5. */
|
|
26
|
+
kaynakHizi?: {
|
|
27
|
+
kapasite: number;
|
|
28
|
+
saniyede: number;
|
|
29
|
+
};
|
|
22
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* İstemci adresi. Sıra: cf-connecting-ip (Cloudflare yazar, istemci değiştiremez), x-real-ip (ters vekil yazar),
|
|
33
|
+
* x-forwarded-for'un sondan `guvenilenVekil`. öğesi. İlk öğe asla kullanılmaz: istemci onu istediği gibi yazabilir.
|
|
34
|
+
* Not: cf-connecting-ip'ye ancak köken sunucuya yalnız Cloudflare üzerinden erişiliyorsa güvenilebilir.
|
|
35
|
+
*/
|
|
36
|
+
export declare function istemciAdresi(basliklar: Headers, guvenilenVekil?: number): string;
|
|
23
37
|
export declare const CORS_BASLIKLARI: Record<string, string>;
|
|
24
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* Süreç içi kova. Adres sayısı sınırlıdır. Tablo dolunca önce süresi geçmiş (jetonu yeniden dolmuş, yani yeni
|
|
40
|
+
* gelen bir adresle aynı durumdaki) kayıtlar silinir; yine doluysa yeni adres reddedilir. Böylece çok sayıda sahte
|
|
41
|
+
* adresle eski kayıtları itip gerçek adreslerin sayacı sıfırlanamaz.
|
|
42
|
+
*/
|
|
25
43
|
export declare class AdresKovasi {
|
|
26
44
|
private kovalar;
|
|
27
45
|
private kapasite;
|
|
@@ -32,5 +50,15 @@ export declare class AdresKovasi {
|
|
|
32
50
|
izin: boolean;
|
|
33
51
|
bekle: number;
|
|
34
52
|
};
|
|
53
|
+
/** Jetonu yeniden kapasiteye dolmuş kayıtları siler. */
|
|
54
|
+
temizle(simdiMs: number): void;
|
|
55
|
+
get boyut(): number;
|
|
35
56
|
}
|
|
57
|
+
/** Gövdeyi bayt sayarak okur; sınır aşılınca akışı iptal eder. content-length olmasa da (chunked) bellek sınırlı kalır. */
|
|
58
|
+
export declare function govdeOku(istek: Request, enBuyuk: number): Promise<{
|
|
59
|
+
tur: "tamam";
|
|
60
|
+
tampon: Uint8Array;
|
|
61
|
+
} | {
|
|
62
|
+
tur: "buyuk";
|
|
63
|
+
}>;
|
|
36
64
|
export declare function acikMcpIsleyici(s?: AcikHttpSecenekleri): (istek: Request) => Promise<Response>;
|
|
@@ -6,6 +6,26 @@
|
|
|
6
6
|
// Sınırlar: gövde en çok 512 KB; adres başına kova (varsayılan 60 istek, saniyede 1 dolar).
|
|
7
7
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
8
8
|
import { acikSunucu } from "./sunucu.js";
|
|
9
|
+
/**
|
|
10
|
+
* İstemci adresi. Sıra: cf-connecting-ip (Cloudflare yazar, istemci değiştiremez), x-real-ip (ters vekil yazar),
|
|
11
|
+
* x-forwarded-for'un sondan `guvenilenVekil`. öğesi. İlk öğe asla kullanılmaz: istemci onu istediği gibi yazabilir.
|
|
12
|
+
* Not: cf-connecting-ip'ye ancak köken sunucuya yalnız Cloudflare üzerinden erişiliyorsa güvenilebilir.
|
|
13
|
+
*/
|
|
14
|
+
export function istemciAdresi(basliklar, guvenilenVekil = 1) {
|
|
15
|
+
const cf = basliklar.get("cf-connecting-ip")?.trim();
|
|
16
|
+
if (cf)
|
|
17
|
+
return cf.slice(0, 64);
|
|
18
|
+
const gercek = basliklar.get("x-real-ip")?.trim();
|
|
19
|
+
if (gercek)
|
|
20
|
+
return gercek.slice(0, 64);
|
|
21
|
+
const liste = (basliklar.get("x-forwarded-for") ?? "")
|
|
22
|
+
.split(",")
|
|
23
|
+
.map((x) => x.trim())
|
|
24
|
+
.filter(Boolean);
|
|
25
|
+
const n = Math.max(1, Math.floor(guvenilenVekil));
|
|
26
|
+
const adres = liste.length >= n ? liste[liste.length - n] : undefined;
|
|
27
|
+
return adres ? adres.slice(0, 64) : "bilinmeyen";
|
|
28
|
+
}
|
|
9
29
|
export const CORS_BASLIKLARI = {
|
|
10
30
|
"access-control-allow-origin": "*",
|
|
11
31
|
"access-control-allow-methods": "POST, GET, OPTIONS",
|
|
@@ -18,7 +38,11 @@ const json = (govde, durum, ek = {}) => new Response(JSON.stringify(govde), {
|
|
|
18
38
|
headers: { "content-type": "application/json; charset=utf-8", ...CORS_BASLIKLARI, ...ek },
|
|
19
39
|
});
|
|
20
40
|
const rpcHata = (durum, code, message, ek = {}) => json({ jsonrpc: "2.0", id: null, error: { code, message } }, durum, ek);
|
|
21
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* Süreç içi kova. Adres sayısı sınırlıdır. Tablo dolunca önce süresi geçmiş (jetonu yeniden dolmuş, yani yeni
|
|
43
|
+
* gelen bir adresle aynı durumdaki) kayıtlar silinir; yine doluysa yeni adres reddedilir. Böylece çok sayıda sahte
|
|
44
|
+
* adresle eski kayıtları itip gerçek adreslerin sayacı sıfırlanamaz.
|
|
45
|
+
*/
|
|
22
46
|
export class AdresKovasi {
|
|
23
47
|
kovalar = new Map();
|
|
24
48
|
kapasite;
|
|
@@ -32,11 +56,10 @@ export class AdresKovasi {
|
|
|
32
56
|
al(adres, simdiMs) {
|
|
33
57
|
let k = this.kovalar.get(adres);
|
|
34
58
|
if (!k) {
|
|
35
|
-
if (this.kovalar.size >= this.enCokAdres)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
59
|
+
if (this.kovalar.size >= this.enCokAdres)
|
|
60
|
+
this.temizle(simdiMs);
|
|
61
|
+
if (this.kovalar.size >= this.enCokAdres)
|
|
62
|
+
return { izin: false, bekle: Math.ceil(1 / this.saniyede) };
|
|
40
63
|
k = { jeton: this.kapasite, son: simdiMs };
|
|
41
64
|
this.kovalar.set(adres, k);
|
|
42
65
|
}
|
|
@@ -47,6 +70,44 @@ export class AdresKovasi {
|
|
|
47
70
|
k.jeton -= 1;
|
|
48
71
|
return { izin: true, bekle: 0 };
|
|
49
72
|
}
|
|
73
|
+
/** Jetonu yeniden kapasiteye dolmuş kayıtları siler. */
|
|
74
|
+
temizle(simdiMs) {
|
|
75
|
+
for (const [a, k] of this.kovalar)
|
|
76
|
+
if (k.jeton + ((simdiMs - k.son) / 1000) * this.saniyede >= this.kapasite)
|
|
77
|
+
this.kovalar.delete(a);
|
|
78
|
+
}
|
|
79
|
+
get boyut() {
|
|
80
|
+
return this.kovalar.size;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** Gövdeyi bayt sayarak okur; sınır aşılınca akışı iptal eder. content-length olmasa da (chunked) bellek sınırlı kalır. */
|
|
84
|
+
export async function govdeOku(istek, enBuyuk) {
|
|
85
|
+
const uzunluk = Number(istek.headers.get("content-length") ?? "0");
|
|
86
|
+
if (uzunluk > enBuyuk)
|
|
87
|
+
return { tur: "buyuk" };
|
|
88
|
+
if (!istek.body)
|
|
89
|
+
return { tur: "tamam", tampon: new Uint8Array(0) };
|
|
90
|
+
const okuyucu = istek.body.getReader();
|
|
91
|
+
const parcalar = [];
|
|
92
|
+
let toplam = 0;
|
|
93
|
+
for (;;) {
|
|
94
|
+
const { done, value } = await okuyucu.read();
|
|
95
|
+
if (done)
|
|
96
|
+
break;
|
|
97
|
+
toplam += value.byteLength;
|
|
98
|
+
if (toplam > enBuyuk) {
|
|
99
|
+
await okuyucu.cancel().catch(() => { });
|
|
100
|
+
return { tur: "buyuk" };
|
|
101
|
+
}
|
|
102
|
+
parcalar.push(value);
|
|
103
|
+
}
|
|
104
|
+
const tampon = new Uint8Array(toplam);
|
|
105
|
+
let i = 0;
|
|
106
|
+
for (const p of parcalar) {
|
|
107
|
+
tampon.set(p, i);
|
|
108
|
+
i += p.byteLength;
|
|
109
|
+
}
|
|
110
|
+
return { tur: "tamam", tampon };
|
|
50
111
|
}
|
|
51
112
|
/** Gövdeden günlüğe yazılacak tek bilgi: yöntem ve (tools/call ya da prompts/get ise) araç ya da istem adı. İçerik okunmaz. */
|
|
52
113
|
function yontemVeArac(govde) {
|
|
@@ -68,7 +129,8 @@ export function acikMcpIsleyici(s = {}) {
|
|
|
68
129
|
const enBuyuk = s.enBuyukGovde ?? 512 * 1024;
|
|
69
130
|
const kova = new AdresKovasi(s.hiz?.kapasite ?? 60, s.hiz?.saniyede ?? 1);
|
|
70
131
|
const gunluk = s.gunluk ?? ((k) => console.info(JSON.stringify({ olay: "acik_mcp", ...k })));
|
|
71
|
-
const
|
|
132
|
+
const guvenilenVekil = s.guvenilenVekil ?? 1;
|
|
133
|
+
const kaynakKovasi = new AdresKovasi(s.kaynakHizi?.kapasite ?? 5, s.kaynakHizi?.saniyede ?? 5 / 60);
|
|
72
134
|
return async (istek) => {
|
|
73
135
|
const bas = performance.now();
|
|
74
136
|
let kayit = { yontem: istek.method, arac: null, boyut: 0 };
|
|
@@ -89,20 +151,17 @@ export function acikMcpIsleyici(s = {}) {
|
|
|
89
151
|
return bitir(json({
|
|
90
152
|
hata: "Bu adres bir MCP sunucusudur. Yapay zekâ istemcinize bağlayıcı olarak ekleyin: https://akredit.org/mcp-kullan",
|
|
91
153
|
}, 405, { allow: "POST, OPTIONS" }));
|
|
92
|
-
const adres = (istek.headers
|
|
154
|
+
const adres = istemciAdresi(istek.headers, guvenilenVekil);
|
|
93
155
|
const h = kova.al(adres, simdi().getTime());
|
|
94
156
|
if (!h.izin)
|
|
95
157
|
return bitir(rpcHata(429, -32000, "Çok fazla istek. Biraz bekleyip yeniden deneyin.", { "retry-after": String(h.bekle) }));
|
|
96
|
-
const uzunluk = Number(istek.headers.get("content-length") ?? "0");
|
|
97
|
-
if (uzunluk > enBuyuk)
|
|
98
|
-
return bitir(rpcHata(413, -32600, "İstek çok büyük. Veriyi bölerek gönderin."));
|
|
99
158
|
let metin;
|
|
100
159
|
try {
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
if (tampon.byteLength > enBuyuk)
|
|
160
|
+
const g = await govdeOku(istek, enBuyuk);
|
|
161
|
+
if (g.tur === "buyuk")
|
|
104
162
|
return bitir(rpcHata(413, -32600, "İstek çok büyük. Veriyi bölerek gönderin."));
|
|
105
|
-
|
|
163
|
+
kayit.boyut = g.tampon.byteLength;
|
|
164
|
+
metin = new TextDecoder().decode(g.tampon);
|
|
106
165
|
}
|
|
107
166
|
catch {
|
|
108
167
|
return bitir(rpcHata(400, -32700, "İstek okunamadı."));
|
|
@@ -115,6 +174,18 @@ export function acikMcpIsleyici(s = {}) {
|
|
|
115
174
|
return bitir(rpcHata(400, -32700, "İleti JSON değil."));
|
|
116
175
|
}
|
|
117
176
|
kayit = { ...kayit, ...yontemVeArac(govde) };
|
|
177
|
+
// Dış katalog sorgusu yapan araç için adres başına ayrı, sıkı kota (toplu istekte her çağrı sayılır).
|
|
178
|
+
const kaynakCagrisi = (Array.isArray(govde) ? govde : [govde]).filter((x) => x &&
|
|
179
|
+
typeof x === "object" &&
|
|
180
|
+
x.method === "tools/call" &&
|
|
181
|
+
x.params?.name === "kaynak_dogrula").length;
|
|
182
|
+
for (let i = 0; i < kaynakCagrisi; i++) {
|
|
183
|
+
const k = kaynakKovasi.al(adres, simdi().getTime());
|
|
184
|
+
if (!k.izin)
|
|
185
|
+
return bitir(rpcHata(429, -32000, "Kaynak doğrulama için kısa sürede çok fazla istek geldi. Bir dakika bekleyip yeniden deneyin.", {
|
|
186
|
+
"retry-after": String(k.bekle),
|
|
187
|
+
}));
|
|
188
|
+
}
|
|
118
189
|
const sunucu = acikSunucu({ simdi });
|
|
119
190
|
const tasima = new WebStandardStreamableHTTPServerTransport({
|
|
120
191
|
sessionIdGenerator: undefined,
|
|
@@ -3,6 +3,7 @@ export * from "./bologna.ts";
|
|
|
3
3
|
export * from "./genel-ayristir.ts";
|
|
4
4
|
export * from "./http.ts";
|
|
5
5
|
export * from "./kaynak.ts";
|
|
6
|
+
export * from "./kaynak-dogrula.ts";
|
|
6
7
|
export * from "./kisisel-veri.ts";
|
|
7
8
|
export * from "./not-tablosu.ts";
|
|
8
9
|
export * from "./promptlar.ts";
|
|
@@ -5,6 +5,7 @@ export * from "./bologna.js";
|
|
|
5
5
|
export * from "./genel-ayristir.js";
|
|
6
6
|
export * from "./http.js";
|
|
7
7
|
export * from "./kaynak.js";
|
|
8
|
+
export * from "./kaynak-dogrula.js";
|
|
8
9
|
export * from "./kisisel-veri.js";
|
|
9
10
|
export * from "./not-tablosu.js";
|
|
10
11
|
export * from "./promptlar.js";
|
|
@@ -2,29 +2,62 @@ import { z } from "zod";
|
|
|
2
2
|
export declare const IZINLI_HOSTLAR: readonly ["api.crossref.org", "openlibrary.org", "www.googleapis.com"];
|
|
3
3
|
export declare const ZAMAN_ASIMI_MS = 8000;
|
|
4
4
|
export declare const EN_BUYUK_YANIT = 1000000;
|
|
5
|
+
/** Çağrı başına en çok kaynak. */
|
|
6
|
+
export declare const EN_COK_KAYNAK = 5;
|
|
7
|
+
/** Süreç genelinde aynı anda en çok dış istek. */
|
|
8
|
+
export declare const ESZAMANLI_DIS_ISTEK = 4;
|
|
9
|
+
/** Süreç genelindeki dış istek semaforu. Sınır doluysa sıra bekler. */
|
|
10
|
+
export declare function sinirli<T>(is: () => Promise<T>): Promise<T>;
|
|
11
|
+
/** Katalog kaydının imzası: künye ve bağlantı üzerinden HMAC-SHA256 (ilk 32 onaltılık hane). */
|
|
12
|
+
export declare function kaynakImzasi(kunye: string, baglanti: string): string;
|
|
13
|
+
/** İmza bu sürecin kaynak_dogrula çıktısına mı ait? */
|
|
14
|
+
export declare function kaynakImzasiDogru(kunye: string, baglanti: string, imza: string): boolean;
|
|
5
15
|
/** "https://doi.org/10.x/y", "doi:10.x/y" ya da "10.x/y" → "10.x/y". Geçersizse null. */
|
|
6
16
|
export declare function doiTemizle(d: string): string | null;
|
|
7
17
|
/** ISBN-10 ya da ISBN-13; tire ve boşluk atılır, denetim basamağı sınanır. Geçersizse null. */
|
|
8
18
|
export declare function isbnTemizle(i: string): string | null;
|
|
9
19
|
/** Karşılaştırma için sadeleştirme: küçük harf, Türkçe harfler düz, noktalama yok. */
|
|
10
20
|
export declare function sadele(s: string): string[];
|
|
11
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* 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ı.
|
|
23
|
+
* Kısa (en çok iki sözcüklü) başlıklarda ("Veri Yapıları", "Genel Kimya") sözcük kümeleri birebir aynı olmalı.
|
|
24
|
+
*/
|
|
12
25
|
export declare function baslikEslesir(sorgu: string, bulunan: string): boolean;
|
|
13
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* Sorgudaki yazar soyadı (son sözcük ya da virgülden önceki kısım) bulunan yazarlardan birinde TAM belirteç olarak
|
|
28
|
+
* geçiyor mu. Alt dize yetmez: "Yılmaz" sorgusu "Yılmazer" ile eşleşmez.
|
|
29
|
+
*/
|
|
14
30
|
export declare function yazarEslesir(sorgu: string, yazarlar: string[]): boolean;
|
|
31
|
+
/** Yanıt gövdesini bayt sayarak okur; sınır aşılırsa akışı iptal edip null döner. */
|
|
32
|
+
export declare function sinirliMetin(y: Response, enBuyuk: number): Promise<string | null>;
|
|
33
|
+
export type KayitTuru = "kitap" | "makale" | "kitap_bolumu" | "diger";
|
|
15
34
|
export interface Dogrulanmis {
|
|
16
|
-
durum: "
|
|
35
|
+
durum: "katalogda_bulundu";
|
|
17
36
|
kunye: string;
|
|
18
37
|
eksikler: string[];
|
|
19
38
|
baglanti: string;
|
|
20
39
|
katalog: "Crossref" | "Open Library" | "Google Books";
|
|
40
|
+
kayitTuru: KayitTuru;
|
|
21
41
|
baslik: string;
|
|
22
42
|
yazarlar: string[];
|
|
23
43
|
yil?: number | string;
|
|
44
|
+
/** Neyle eşleşti: DOI, ISBN ya da başlık + tam soyad (+ yıl, tür). */
|
|
45
|
+
kimlik_eslesmesi: {
|
|
46
|
+
yontem: "doi" | "isbn" | "baslik_yazar";
|
|
47
|
+
denetlenen: string[];
|
|
48
|
+
};
|
|
49
|
+
/** Künyede eksik alan var mı (yayınevi, yıl...). Eksik alan [EKSİK] kalır. */
|
|
50
|
+
bibliyografik_tamlik: "tam" | "eksik";
|
|
51
|
+
/** Katalogda bulunmak, ders kaynağı olarak uygun olmak demek değildir. */
|
|
52
|
+
ders_kaynagi_uygunlugu: "hoca karar verir";
|
|
53
|
+
/** Bologna araçlarında doğrulanmış kaynak kanıtı. */
|
|
54
|
+
imza: string;
|
|
24
55
|
}
|
|
25
56
|
export interface Dogrulanamadi {
|
|
26
|
-
durum: "
|
|
57
|
+
durum: "bulunamadi";
|
|
27
58
|
neden: string;
|
|
59
|
+
/** Hoca eseri kendisi biliyorsa yalnız "hoca_beyani" etiketiyle yazılabilir. */
|
|
60
|
+
hocaBeyani: string;
|
|
28
61
|
}
|
|
29
62
|
export type DogrulamaSonucu = Dogrulanmis | Dogrulanamadi;
|
|
30
63
|
declare const KaynakSorgusu: z.ZodObject<{
|
|
@@ -32,6 +65,13 @@ declare const KaynakSorgusu: z.ZodObject<{
|
|
|
32
65
|
isbn: z.ZodOptional<z.ZodString>;
|
|
33
66
|
baslik: z.ZodOptional<z.ZodString>;
|
|
34
67
|
yazar: z.ZodOptional<z.ZodString>;
|
|
68
|
+
tur: z.ZodOptional<z.ZodEnum<{
|
|
69
|
+
kitap: "kitap";
|
|
70
|
+
makale: "makale";
|
|
71
|
+
kitap_bolumu: "kitap_bolumu";
|
|
72
|
+
herhangi: "herhangi";
|
|
73
|
+
}>>;
|
|
74
|
+
yil: z.ZodOptional<z.ZodNumber>;
|
|
35
75
|
}, z.core.$strip>;
|
|
36
76
|
type KaynakSorgusuT = z.infer<typeof KaynakSorgusu>;
|
|
37
77
|
/** Tek kaynağı doğrular. DOI önce, sonra ISBN, sonra başlık+yazar. */
|
|
@@ -42,6 +82,13 @@ export declare const kaynakDogrulaAraci: import("./temel.ts").AcikArac<z.ZodObje
|
|
|
42
82
|
isbn: z.ZodOptional<z.ZodString>;
|
|
43
83
|
baslik: z.ZodOptional<z.ZodString>;
|
|
44
84
|
yazar: z.ZodOptional<z.ZodString>;
|
|
85
|
+
tur: z.ZodOptional<z.ZodEnum<{
|
|
86
|
+
kitap: "kitap";
|
|
87
|
+
makale: "makale";
|
|
88
|
+
kitap_bolumu: "kitap_bolumu";
|
|
89
|
+
herhangi: "herhangi";
|
|
90
|
+
}>>;
|
|
91
|
+
yil: z.ZodOptional<z.ZodNumber>;
|
|
45
92
|
}, z.core.$strip>>;
|
|
46
93
|
}, z.core.$strip>>;
|
|
47
94
|
export {};
|