@appaflytech/zeyrek-ai-mcp 0.0.52 → 0.0.54
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/dist/knowledge/componentCatalog.js +3 -1
- package/dist/knowledge/componentEvents.js +13 -1
- package/dist/knowledge/mobileComponentCatalog.js +4 -1
- package/dist/knowledge/normalize.js +4 -3
- package/dist/platform/argumentAliases.js +13 -4
- package/dist/platform/bodyFile.js +195 -38
- package/dist/platform/capture/actions.js +24 -5
- package/dist/platform/capture/capture.js +80 -11
- package/dist/platform/capture/persistent-browser.js +23 -1
- package/dist/platform/capture/readiness.js +32 -1
- package/dist/platform/errors.js +45 -5
- package/dist/platform/http.js +17 -9
- package/dist/platform/responses.js +7 -1
- package/dist/platform/secrets.js +8 -0
- package/dist/platform/token.js +32 -21
- package/dist/resources/examples/mobile-offline-list.json +1 -1
- package/dist/resources/examples/mobile-offline-queue.json +1 -1
- package/dist/resources/examples/mobile-offline-seamless.json +12 -3
- package/dist/resources/guides/deployment.md +9 -0
- package/dist/resources/guides/merge.md +5 -0
- package/dist/resources/guides/microservice.md +13 -1
- package/dist/resources/guides/mobile-publish.md +7 -4
- package/dist/resources/guides/mobile-steps.md +40 -3
- package/dist/resources/guides/mobile.md +39 -8
- package/dist/resources/guides/page.md +39 -8
- package/dist/resources/guides/translation.md +3 -1
- package/dist/resources/prompts/mobile-publish-prepare.md +3 -2
- package/dist/resources/schema-fingerprint.json +2 -2
- package/dist/tools/applications.js +9 -4
- package/dist/tools/connectData.js +13 -4
- package/dist/tools/deployments.js +1 -1
- package/dist/tools/designSchemaPatch.js +7 -2
- package/dist/tools/environments.js +40 -0
- package/dist/tools/flows.js +30 -3
- package/dist/tools/microservices.js +25 -7
- package/dist/tools/mobile.js +56 -0
- package/dist/tools/mobileStore.js +2 -2
- package/dist/tools/pages.js +69 -16
- package/dist/tools/platformInfo.js +1 -1
- package/dist/tools/screenWrite.js +107 -1
- package/dist/tools/variables.js +1 -1
- package/package.json +1 -1
- package/dist/resources/guides/mobile-flow.md +0 -64
|
@@ -502,6 +502,7 @@ export const componentKnowledge = {
|
|
|
502
502
|
summary: 'Aranabilir çoklu seçim; value bir değer dizisidir, seçilenler etiket (chip) olarak görünür, search olayı sunucu taraflı arama kurmaya yarar.',
|
|
503
503
|
behavior: [
|
|
504
504
|
"Bakma penceresi (lookupMode modal | drawer) tablo kelimelerini (Uygula, Seçimi temizle, ipucu satırı, filtre çubuğu, işleç adları) tablonun satırlarından okur: compProp.datatable.<kelime> (2026-10-15, frontend ADR 0024); bu satırlar bileşenin Translations sekmesinde tablonun anahtarlarıyla listelenir ve bileşen kullanılınca tohumlanır. Düğümün texts'i önce gelir.",
|
|
505
|
+
"Bakma penceresinde başlık satırının ilk hücresi tümünü seç kutusudur (2026-11-02): süzgece uyan BÜTÜN kayıtları seçer, sonuç sayfaya sığmıyorsa kimlikleri bir kez okur (en çok 1000; fazlasında pencere söyler). Süzgecin dışındaki önceki seçim korunur, yazım yine Uygula iledir. Datatable çoka-çok hücresi ve çoklu filtre penceresi aynı pencereyi kullanır.",
|
|
505
506
|
"value bir DİZİDİR ve üç biçimi de okunur: düz değer (['policies', 7]), şemanın biçimi ([{ value, label }]) ve eski ikizler ([{ id, text }]). Düz değer yazmak 2026-09-11'e kadar etiketi 'undefined' gösteriyordu; artık değer etiketin kendisi olur ve doğru metin options yüklenince gelir (2026-10-01'e kadar gelmiyordu: akışın yazdığı id dizisi seçenekler yüklense de çipte id olarak kalıyordu).",
|
|
506
507
|
"Etiketin metni options'tan tazelenir: aynı value'lu option listede varsa onun label'ı yazılır. Bu arama METİNLEŞTİREREK karşılaştırır (String(a) === String(b)), AMA yalnız etiket metni için: satırın işaretli görünmesi ve seçimin kaldırılması KATI karşılaştırma kullanır. Yani value'da metin '34', options'ta sayı 34 varsa çip doğru etiketle çizilir ama liste satırı işaretsiz kalır ve tıklamak hiçbir şey yapmaz — eşlemeyi akış katmanında aynı tipe getir (CQ-208).",
|
|
507
508
|
"change yükü { value: [{ value, label, id, text }] } taşır ve düğümün value prop'una yazılır: akışta self.value ile okunur. id/text, value/label'ın eski adlı ikizleridir — kayıtlı sayfalar onlarla okuduğu için bilerek korunuyor.",
|
|
@@ -578,7 +579,8 @@ export const componentKnowledge = {
|
|
|
578
579
|
maxChars: "SERT sınır (tarayıcı maxlength): fazlası yazılamaz. Akıştan gelen daha uzun değer aynen gösterilir (sayaç 45/40 okur). validations.maxLength'ten FARKLIDIR: o yalnız doğrular. 0/boş = sınırsız.",
|
|
579
580
|
showCounter: "Kutunun altında sağa dayalı sayaç: maxChars varsa '12/40', yoksa '12'.",
|
|
580
581
|
autofocus: 'Sayfa açılınca odak bu alana gelir (dondurulmuş alanda ve tasarım kanvasında çalışmaz). Sayfada tek alana ver.',
|
|
581
|
-
isLoading: "Alanı dondurur (readonly, gri değil) ve sağ üstte spinner çizer; changed'e bağlı sorgu sürerken aç, tamamlanınca kapat."
|
|
582
|
+
isLoading: "Alanı dondurur (readonly, gri değil) ve sağ üstte spinner çizer; changed'e bağlı sorgu sürerken aç, tamamlanınca kapat.",
|
|
583
|
+
selection: "selection { start, end } (f-07): kullanıcı seçince ya da imleci taşıyınca selectionChange yazar; yazarken kayan imleç yalnız prop'u günceller (akış koşmaz). Alana hiç dokunulmadıysa undefined'dır. Akış Components.<id>.selection'a yazarsa alan seçimi/imleci oraya koyar ve odaklanır (aynı akışta value da yazıldıysa yeni metne göre). Seçili parçayı biçimlemek ('Kalın' düğmesinin click akışı): const v = Components.<alan>.value ?? ''; const s = Components.<alan>.selection ?? { start: v.length, end: v.length }; const yeni = v.slice(0, s.start) + '**' + v.slice(s.start, s.end) + '**' + v.slice(s.end); const imlec = s.start === s.end ? s.start + 2 : s.end + 4; sonra setter ile value = yeni, selection = { start: imlec, end: imlec } (seçimde imleç kapanış işaretinden sonra, boş imleçte iki işaretin arasında). Web ve mobil aynı yükü verir; textinput'ta bu olay YOK."
|
|
582
584
|
}
|
|
583
585
|
},
|
|
584
586
|
texteditor: {
|
|
@@ -125,7 +125,19 @@ export const componentEventContracts = {
|
|
|
125
125
|
focus: NO_DATA,
|
|
126
126
|
blur: NO_DATA
|
|
127
127
|
},
|
|
128
|
-
textarea: {
|
|
128
|
+
textarea: {
|
|
129
|
+
mount: NO_DATA,
|
|
130
|
+
change: valueChange('string'),
|
|
131
|
+
changed: valueChange('string (blur ile kesinleşen değer)'),
|
|
132
|
+
focus: NO_DATA,
|
|
133
|
+
blur: NO_DATA,
|
|
134
|
+
// f-07 (plans/defter-saha-bulgulari): yük hem prop'a (`self.selection`) hem `params` ile `event.data`ya gider.
|
|
135
|
+
selectionChange: {
|
|
136
|
+
payload: {
|
|
137
|
+
selection: '{ start, end } — seçimin başı ve sonu, UTF-16 indeks (JS `slice` ile aynı birim); imleçte start === end. Aynı değer event.data.start / event.data.end olarak da gelir. Kullanıcı seçince ya da imleci taşıyınca yayılır (iki platformda 50 ms içindeki değişimler tek olay). Yazarken kayan imleç akış KOŞTURMAZ: yalnız self.selection güncellenir (her tuşta akış olmasın); biçim düğmesi seçimi tıklamada Components.<id>.selection\'dan okur'
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
},
|
|
129
141
|
password: {
|
|
130
142
|
mount: NO_DATA,
|
|
131
143
|
change: valueChange('string'),
|
|
@@ -198,7 +198,10 @@ export const mobileComponentKnowledge = {
|
|
|
198
198
|
differences: [
|
|
199
199
|
WEB_WORDS_NOTE,
|
|
200
200
|
'Yükseklik açıkça verilir (React Native `TextInput` satır sayısını kendi ölçmez): `autoGrow` (web varsayılanı AÇIK) iken içerikle `minLines`–`maxLines` arasında büyür, kapalıyken sabit `lines`. 2026-09-27\'ye dek hep sabit boydu (f-05). Satır yüksekliği `punto × 1,45` stile yazılır ve satır tabanlı boylar iç boşluk + kenarlığı içerir: `maxLines: 5` beş tam satır gösterir (2026-09-29). `autofocus` runtime\'da klavyeyi açar.'
|
|
201
|
-
]
|
|
201
|
+
],
|
|
202
|
+
propNotes: {
|
|
203
|
+
selection: "Web ile aynı sözleşme (componentCatalog textarea.propNotes.selection). Cihazda seçim `onSelectionChange`'ten gelir ve 50 ms içinde tek `selectionChange`'e katlanır; akışın yazdığı seçim `setSelection` ile konur ve alan odaklanır (klavye açılır). Studio önizlemesinde (react-native-web) aynı yol `setSelectionRange`."
|
|
204
|
+
}
|
|
202
205
|
},
|
|
203
206
|
numberinput: {
|
|
204
207
|
summary: 'Sayısal klavye açan alan; değer `Intl.NumberFormat` ile biçimlenir.',
|
|
@@ -1669,11 +1669,12 @@ const SCREEN_EVENT_NAMES = ['mount', 'destroy'];
|
|
|
1669
1669
|
* koşmaz. Web her gezinmede sayfayı baştan kurar (`Runtime.svelte` focus'u tanımaz), layout'un focus'u
|
|
1670
1670
|
* yoktur. Ayna: `ng-apps-ui-native` `SCREEN_LIFECYCLE_EVENTS` (parite: `test/page-meta.test.ts`).
|
|
1671
1671
|
*/
|
|
1672
|
-
const MOBILE_PAGE_EVENT_NAMES = [...SCREEN_EVENT_NAMES, 'focus'];
|
|
1672
|
+
const MOBILE_PAGE_EVENT_NAMES = [...SCREEN_EVENT_NAMES, 'focus', 'offlineQueueChange'];
|
|
1673
1673
|
const SCREEN_EVENT_HINTS = {
|
|
1674
1674
|
mount: 'mount (yüklendiğinde)',
|
|
1675
1675
|
destroy: 'destroy (ayrılırken; temizlik akışları için)',
|
|
1676
|
-
focus: 'focus (ekran her göründüğünde — ilk açılış ve geri dönüş; listeyi yeniden okumak için)'
|
|
1676
|
+
focus: 'focus (ekran her göründüğünde — ilk açılış ve geri dönüş; listeyi yeniden okumak için)',
|
|
1677
|
+
offlineQueueChange: 'offlineQueueChange (servis yazma kuyruğu değişince — ağsız kayıt girdi, gönderildi, reddedildi; listeyi yeniden okumak için, sayaç Device.offline)'
|
|
1677
1678
|
};
|
|
1678
1679
|
export function screenEventNames(kind, platform) {
|
|
1679
1680
|
return kind === 'page' && platform === 'mobile' ? MOBILE_PAGE_EVENT_NAMES : SCREEN_EVENT_NAMES;
|
|
@@ -1688,7 +1689,7 @@ export function invalidScreenEventNames(events, code, names = SCREEN_EVENT_NAMES
|
|
|
1688
1689
|
.map(event => String(event.name ?? ''))
|
|
1689
1690
|
.filter(name => name !== '' && !names.includes(name))
|
|
1690
1691
|
.map(name => MOBILE_PAGE_EVENT_NAMES.includes(name)
|
|
1691
|
-
? `${code}: '${name}' yalnız mobil uygulamanın SAYFASINDA vardır (layout'ta ve web sayfasında yok; web sayfayı her gezinmede baştan kurar, mount yeter) — burada geçerli adlar: ${valid}.`
|
|
1692
|
+
? `${code}: '${name}' yalnız mobil uygulamanın SAYFASINDA vardır (layout'ta ve web sayfasında yok; ${name === 'offlineQueueChange' ? 'web barındırıcısı servis yazma kuyruğu tutmaz, olay hiç yayılmaz' : 'web sayfayı her gezinmede baştan kurar, mount yeter'}) — burada geçerli adlar: ${valid}.`
|
|
1692
1693
|
: `${code}: sayfa/layout düzeyinde '${name}' diye bir olay YOK — geçerli adlar: ${valid}. Platform kaydı kabul eder ama bağ sessizce ölü kalır.`);
|
|
1693
1694
|
}
|
|
1694
1695
|
const GUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
@@ -100,6 +100,14 @@ export async function applyArgumentAliases(tool, args, context) {
|
|
|
100
100
|
* harf duyarsız düzenleme uzaklığı 2'yi geçmeyen en yakın ad (`hostID` → `hostId`, `views` → `view`).
|
|
101
101
|
*/
|
|
102
102
|
export function closestArgument(key, accepted) {
|
|
103
|
+
return closestNames(key, accepted, 1, 2)[0];
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* `key`'e en yakın `limit` ad (büyük/küçük harf duyarsız düzenleme uzaklığı, eşitlikte `accepted` sırası). `maxDistance`
|
|
107
|
+
* verilmezse uzaklık sınırı yoktur: g-21'de micro-op'un tanınmayan prop'una komponentin en yakın üç prop'u önerilir
|
|
108
|
+
* (`ariaLabel` → `label`), o adlar 2'den uzak olsa da sözlüğün nereye baktığını gösterir.
|
|
109
|
+
*/
|
|
110
|
+
export function closestNames(key, accepted, limit, maxDistance = Number.POSITIVE_INFINITY) {
|
|
103
111
|
const distance = (left, right) => {
|
|
104
112
|
const row = Array.from({ length: right.length + 1 }, (_, index) => index);
|
|
105
113
|
for (let i = 1; i <= left.length; i++) {
|
|
@@ -113,11 +121,12 @@ export function closestArgument(key, accepted) {
|
|
|
113
121
|
}
|
|
114
122
|
return row[right.length] ?? 0;
|
|
115
123
|
};
|
|
116
|
-
|
|
124
|
+
return accepted
|
|
117
125
|
.map(name => ({ name, score: distance(key.toLowerCase(), name.toLowerCase()) }))
|
|
118
|
-
.filter(entry => entry.score <=
|
|
119
|
-
.sort((left, right) => left.score - right.score)
|
|
120
|
-
|
|
126
|
+
.filter(entry => entry.score <= maxDistance)
|
|
127
|
+
.sort((left, right) => left.score - right.score)
|
|
128
|
+
.slice(0, limit)
|
|
129
|
+
.map(entry => entry.name);
|
|
121
130
|
}
|
|
122
131
|
const activeNotes = new AsyncLocalStorage();
|
|
123
132
|
export function runWithArgumentNotes(notes, run) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { open, realpath } from 'node:fs/promises';
|
|
1
|
+
import { open, realpath, stat } from 'node:fs/promises';
|
|
2
2
|
import { constants } from 'node:fs';
|
|
3
|
-
import { extname, resolve, sep } from 'node:path';
|
|
3
|
+
import { dirname, extname, isAbsolute, resolve, sep } from 'node:path';
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
import { PlatformApiError } from './errors.js';
|
|
6
6
|
/**
|
|
@@ -27,77 +27,234 @@ export const BODY_FILE_ARGUMENTS = {
|
|
|
27
27
|
/** `ops`/`operations` eşanlamlıdır (`microOps`); ikisinden biri satır içinde varsa gövde verilmiştir. */
|
|
28
28
|
const synonyms = { ops: ['ops', 'operations'], operations: ['ops', 'operations'] };
|
|
29
29
|
const maxBodyFileBytes = 5 * 1024 * 1024;
|
|
30
|
+
/**
|
|
31
|
+
* g-22 (D-22, K8 — `plans/defter-saha-bulgulari`): akış kodu JSON metninin içinde kaçışlı dizgi olarak duruyordu;
|
|
32
|
+
* Defter'de bir ters tırnak hatası düzenleri kırdı ve iş JS üreteçlerine taşındı. `{ "$file": "./adim.js" }` o
|
|
33
|
+
* dosyanın METNİYLE değiştirilir — gateway kodu ÇALIŞTIRMAZ (reddedilen seçenek: gateway'de .js koşturmak). Eklenen
|
|
34
|
+
* dosya bodyFile'ın bütün denetimlerinden geçer; sınırlar sahibin onayladığı K8 değerleridir.
|
|
35
|
+
*/
|
|
36
|
+
const includedExtensions = ['.js', '.mjs', '.ts', '.txt', '.sql', '.html', '.css'];
|
|
37
|
+
const maxIncludedFileBytes = 1024 * 1024;
|
|
38
|
+
const maxInclusions = 64;
|
|
30
39
|
export function bodyFileParam(argument) {
|
|
31
|
-
return z.string().min(1).optional().describe(`'${argument}' yerine okunacak yerel .json dosyası (en çok 5 MB): gövde ajan bağlamından geçmez, gateway dosyayı okuyup '${argument}' olarak aynı doğrulamadan geçirir. '${argument}' ile birlikte verilemez.
|
|
40
|
+
return z.string().min(1).optional().describe(`'${argument}' yerine okunacak yerel .json dosyası (en çok 5 MB): gövde ajan bağlamından geçmez, gateway dosyayı okuyup '${argument}' olarak aynı doğrulamadan geçirir. '${argument}' ile birlikte verilemez. Göreli yol .env'deki MCP_INPUT_DIR'e (tanımsızsa gateway'in çalışma dizinine) göre çözülür; dosya bu iki dizinden birinin altında olmalı (sembolik bağ çözülür). JSON'un herhangi bir yerine { "$file": "./adim.js" } yazılırsa o dosyanın METNİ oraya konur — kod çalıştırılmaz; yol bodyFile'ın klasörüne göre çözülür; ${includedExtensions.join(' ')}, UTF-8, dosya başına 1 MB, toplam 5 MB, en çok ${maxInclusions} ekleme; eklenen dosyanın içindeki $file işlenmez. Biçim tam { "$file": "<yol>" }'dur (tek anahtar, dizgi) — bozuk biçim ve satır içi gövdedeki $file istek gitmeden reddedilir. Akış code adımının args.value'su böylece kaçışsız düz JS dosyasında durur. Yanıtın bodyFile alanı okunan dosyanın mutlak yolunu, kökünü (root, rootSource) ve eklenen dosyaları (included: at, path, bytes) söyler.`);
|
|
32
41
|
}
|
|
33
42
|
export async function loadBodyFile(tool, args, config) {
|
|
34
43
|
const argument = BODY_FILE_ARGUMENTS[tool];
|
|
35
44
|
if (!argument || !args || typeof args !== 'object' || Array.isArray(args))
|
|
36
45
|
return { args };
|
|
37
46
|
const { bodyFile, ...rest } = args;
|
|
38
|
-
if (bodyFile === undefined)
|
|
39
|
-
return { args };
|
|
40
|
-
if (typeof bodyFile !== 'string' || bodyFile.length === 0)
|
|
41
|
-
return { args };
|
|
42
47
|
const inline = (synonyms[argument] ?? [argument]).filter(name => rest[name] !== undefined);
|
|
48
|
+
if (typeof bodyFile !== 'string' || bodyFile.length === 0) {
|
|
49
|
+
assertNoInlineInclusions(tool, inline, rest);
|
|
50
|
+
return { args };
|
|
51
|
+
}
|
|
43
52
|
if (inline.length > 0) {
|
|
44
53
|
throw new PlatformApiError(`${tool}: bodyFile ile '${inline.join("', '")}' birlikte verilemez — gövde ya dosyadan ya satır içinden gelir. Birini kaldırın. İstek gönderilmedi.`, 400);
|
|
45
54
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
55
|
+
// g-21 (D-21): göreli yol gateway sürecinin çalışma dizinine göre çözülüyordu ve ajan o dizini bilmez. Ajanın
|
|
56
|
+
// bildiği kök .env'deki MCP_INPUT_DIR'dir; tanımsızsa eski davranış (çalışma dizini) sürer.
|
|
57
|
+
const base = config.inputDirectory ? { source: 'MCP_INPUT_DIR', path: resolve(config.inputDirectory) } : { source: 'cwd', path: process.cwd() };
|
|
58
|
+
const roots = await allowedRoots(config);
|
|
59
|
+
const body = await readGuardedFile(tool, 'bodyFile', resolve(base.path, bodyFile), ['.json'], maxBodyFileBytes, roots, config);
|
|
60
|
+
let value;
|
|
61
|
+
try {
|
|
62
|
+
value = JSON.parse(body.buffer.toString('utf8'));
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
throw new PlatformApiError(`${tool}: bodyFile JSON değil — ${body.real} (${error instanceof Error ? error.message : String(error)}). İstek gönderilmedi.`, 400);
|
|
66
|
+
}
|
|
67
|
+
const included = await includeFiles(tool, value, dirname(body.real), body.buffer.length, roots, config);
|
|
68
|
+
const root = isAbsolute(bodyFile) ? body.root : { source: base.source, path: await realpath(base.path).catch(() => base.path) };
|
|
69
|
+
return {
|
|
70
|
+
args: { ...rest, [argument]: included.value },
|
|
71
|
+
note: {
|
|
72
|
+
path: body.real,
|
|
73
|
+
root: root.path,
|
|
74
|
+
rootSource: root.source,
|
|
75
|
+
argument,
|
|
76
|
+
bytes: body.buffer.length,
|
|
77
|
+
...(included.files.length > 0 ? { included: included.files } : {})
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/** MCP_INPUT_DIR önce gelir: dosya iki kökün de altındaysa yanıt ajanın bildiği kökü söyler. */
|
|
82
|
+
async function allowedRoots(config) {
|
|
83
|
+
const candidates = [
|
|
84
|
+
...(config.inputDirectory ? [{ source: 'MCP_INPUT_DIR', path: config.inputDirectory }] : []),
|
|
85
|
+
{ source: 'cwd', path: process.cwd() }
|
|
86
|
+
];
|
|
87
|
+
const resolved = await Promise.all(candidates.map(async (root) => ({ source: root.source, path: await realpath(resolve(root.path)).catch(() => undefined) })));
|
|
88
|
+
return resolved.filter((root) => root.path !== undefined);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* bodyFile'ın ve her `$file` eklemesinin ORTAK okuma kapısı: uzantı (yol ve bağın hedefi), izinli kök, düzenli dosya
|
|
92
|
+
* ve bayt sınırı. O_NONBLOCK FIFO'nun çağrıyı bekletmesini önler; boyut dosyanın büyüklüğüne değil okunan bayta bakar.
|
|
93
|
+
*
|
|
94
|
+
* `realpath` ile `open` atomik DEĞİLDİR: O_NOFOLLOW yalnız SON bileşenin sonradan bağa çevrilmesini önler, yolun
|
|
95
|
+
* ortasındaki bir klasör denetimden sonra dışarıyı gösteren bağa çevrilirse `open` dışarıdaki dosyayı açar (g-22
|
|
96
|
+
* incelemesi, `test/body-file-race.test.ts`). Açtıktan sonra tutamacın aygıt/düğümü yolun taze `stat`'ıyla
|
|
97
|
+
* karşılaştırılır ve `realpath` yeniden koşturulur: yol aynı yere çözülmüyor ya da izinli kökün dışına düşüyorsa okuma
|
|
98
|
+
* reddedilir. Bu pencereyi DARALTIR, kapatmaz — iki denetim arasında geri çevrilen bir bağ yine görülmez; tam çözüm
|
|
99
|
+
* (`openat2` RESOLVE_BENEATH) Node'da yoktur. Okunan bayt yine de açılan tutamaçtan gelir.
|
|
100
|
+
*/
|
|
101
|
+
async function readGuardedFile(tool, label, absolute, extensions, maxBytes, roots, config) {
|
|
102
|
+
const accepted = extensions.join(', ');
|
|
103
|
+
if (!extensions.includes(extname(absolute).toLowerCase())) {
|
|
104
|
+
throw new PlatformApiError(`${tool}: ${label} yalnız ${accepted} okur — verilen: ${absolute}. İstek gönderilmedi.`, 400);
|
|
49
105
|
}
|
|
50
106
|
let real;
|
|
51
107
|
try {
|
|
52
108
|
real = await realpath(absolute);
|
|
53
109
|
}
|
|
54
110
|
catch (error) {
|
|
55
|
-
throw new PlatformApiError(`${tool}:
|
|
111
|
+
throw new PlatformApiError(`${tool}: ${label} okunamadı — ${absolute} (${error instanceof Error ? error.message : String(error)}). Yol gateway sürecinin çalıştığı makineye göre çözülür. İstek gönderilmedi.`, 400);
|
|
56
112
|
}
|
|
57
|
-
if (extname(real).toLowerCase()
|
|
58
|
-
throw new PlatformApiError(`${tool}:
|
|
113
|
+
if (!extensions.includes(extname(real).toLowerCase())) {
|
|
114
|
+
throw new PlatformApiError(`${tool}: ${label} hedefi ${accepted} değil — ${real} (${absolute} bağının hedefi). İstek gönderilmedi.`, 400);
|
|
59
115
|
}
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const allowed = roots.some(root => root !== undefined && (real === root || real.startsWith(`${root}${sep}`)));
|
|
64
|
-
if (!allowed) {
|
|
65
|
-
throw new PlatformApiError(`${tool}: bodyFile izinli dizinlerin dışında — ${real}${real !== absolute ? ` (${absolute} bağının hedefi)` : ''}. Gateway yalnız çalışma dizininin (${process.cwd()}) ve .env'deki MCP_INPUT_DIR'in (${config.inputDirectory ?? 'tanımsız'}) altındaki dosyaları okur. İstek gönderilmedi.`, 400);
|
|
116
|
+
const root = roots.find(candidate => real === candidate.path || real.startsWith(`${candidate.path}${sep}`));
|
|
117
|
+
if (!root) {
|
|
118
|
+
throw new PlatformApiError(`${tool}: ${label} izinli dizinlerin dışında — ${real}${real !== absolute ? ` (${absolute} bağının hedefi)` : ''}. Gateway yalnız çalışma dizininin (${process.cwd()}) ve .env'deki MCP_INPUT_DIR'in (${config.inputDirectory ?? 'tanımsız'}) altındaki dosyaları okur. İstek gönderilmedi.`, 400);
|
|
66
119
|
}
|
|
67
|
-
// Denetim ve okuma AYNI tutamaçtan: yol denetimden sonra değişse de okunan, denetlenen dosyadır.
|
|
68
|
-
// O_NOFOLLOW son bileşenin sonradan bağa çevrilmesini, O_NONBLOCK `.json` adlı FIFO'nun çağrıyı
|
|
69
|
-
// bekletmesini önler; boyut dosyanın büyüklüğüne değil okunan bayta bakar.
|
|
70
120
|
let handle;
|
|
71
121
|
try {
|
|
72
122
|
handle = await open(real, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
73
123
|
}
|
|
74
124
|
catch (error) {
|
|
75
|
-
throw new PlatformApiError(`${tool}:
|
|
125
|
+
throw new PlatformApiError(`${tool}: ${label} açılamadı — ${real} (${error instanceof Error ? error.message : String(error)}). İstek gönderilmedi.`, 400);
|
|
76
126
|
}
|
|
77
|
-
let bytes;
|
|
78
|
-
let text;
|
|
79
127
|
try {
|
|
80
128
|
const info = await handle.stat();
|
|
81
129
|
if (!info.isFile()) {
|
|
82
|
-
throw new PlatformApiError(`${tool}:
|
|
130
|
+
throw new PlatformApiError(`${tool}: ${label} düzenli dosya değil — ${real}. İstek gönderilmedi.`, 400);
|
|
131
|
+
}
|
|
132
|
+
const fresh = await stat(real).catch(() => undefined);
|
|
133
|
+
const again = await realpath(absolute).catch(() => undefined);
|
|
134
|
+
const stillInside = again !== undefined && (again === root.path || again.startsWith(`${root.path}${sep}`));
|
|
135
|
+
if (!fresh || fresh.dev !== info.dev || fresh.ino !== info.ino || again !== real || !stillInside) {
|
|
136
|
+
throw new PlatformApiError(`${tool}: ${label} yolu denetimden sonra değişti — ${real} artık ${again ?? 'çözülemeyen bir yere'} gösteriyor (araya bir bağ girdi). Okunmadı. İstek gönderilmedi.`, 400);
|
|
83
137
|
}
|
|
84
|
-
const buffer = Buffer.alloc(
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
138
|
+
const buffer = Buffer.alloc(maxBytes + 1);
|
|
139
|
+
let bytesRead = 0;
|
|
140
|
+
// Tek `read` dosyanın tamamını döndürmek zorunda değildir; EOF'a ya da sınırın bir baytı ötesine kadar okunur.
|
|
141
|
+
while (bytesRead < buffer.length) {
|
|
142
|
+
const chunk = await handle.read(buffer, bytesRead, buffer.length - bytesRead, bytesRead);
|
|
143
|
+
if (chunk.bytesRead === 0)
|
|
144
|
+
break;
|
|
145
|
+
bytesRead += chunk.bytesRead;
|
|
89
146
|
}
|
|
90
|
-
|
|
147
|
+
if (bytesRead > maxBytes) {
|
|
148
|
+
throw new PlatformApiError(maxBytes === maxBodyFileBytes
|
|
149
|
+
? `${tool}: ${label} 5 MB sınırını aşıyor. Tanımı parçalara bölün — sayfa için page_schema_patch'e birden çok batch, akış için flow_patch. İstek gönderilmedi.`
|
|
150
|
+
: `${tool}: ${label} ${maxBytes / (1024 * 1024)} MB sınırını aşıyor — ${real}. Kodu birden çok dosyaya bölün. İstek gönderilmedi.`, 400);
|
|
151
|
+
}
|
|
152
|
+
return { real, root, buffer: buffer.subarray(0, bytesRead) };
|
|
91
153
|
}
|
|
92
154
|
finally {
|
|
93
155
|
await handle.close();
|
|
94
156
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
157
|
+
}
|
|
158
|
+
/** Kök JSON Pointer'ı boş dizgidir; mesajda `''` ile `/` (adı boş anahtar) karışmasın diye adıyla yazılır. */
|
|
159
|
+
function pointerLabel(at) {
|
|
160
|
+
return at === '' ? '(kök)' : at;
|
|
161
|
+
}
|
|
162
|
+
function pointerToken(key) {
|
|
163
|
+
return String(key).replace(/~/g, '~0').replace(/\//g, '~1');
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Anahtarlarından biri büyük/küçük harf duyarsız `$file` olan her nesne ekleme NİYETİDİR. Yalnız tam
|
|
167
|
+
* `{ "$file": "<yol>" }` biçimi eklemedir; dizi/sayı değer, ek anahtar ya da `$File` yazımı sessizce nesne olarak
|
|
168
|
+
* platforma gitmesin diye `malformed`'a düşer (g-22 incelemesi 2).
|
|
169
|
+
*/
|
|
170
|
+
function findInclusions(value, at, found, malformed) {
|
|
171
|
+
if (Array.isArray(value)) {
|
|
172
|
+
value.forEach((item, index) => findInclusions(item, `${at}/${index}`, found, malformed));
|
|
173
|
+
return;
|
|
98
174
|
}
|
|
99
|
-
|
|
100
|
-
|
|
175
|
+
if (!value || typeof value !== 'object')
|
|
176
|
+
return;
|
|
177
|
+
const record = value;
|
|
178
|
+
const keys = Object.keys(record);
|
|
179
|
+
const marker = keys.find(key => key.toLowerCase() === '$file');
|
|
180
|
+
if (marker !== undefined) {
|
|
181
|
+
if (keys.length === 1 && marker === '$file' && typeof record.$file === 'string') {
|
|
182
|
+
found.push({ at, file: record.$file });
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
const problem = marker !== '$file'
|
|
186
|
+
? `anahtar '${marker}' yazılmış, '$file' olmalı`
|
|
187
|
+
: keys.length > 1
|
|
188
|
+
? `'$file' yanında başka anahtar var (${keys.filter(key => key !== '$file').join(', ')})`
|
|
189
|
+
: `'$file' değeri dizgi değil (${JSON.stringify(record[marker])})`;
|
|
190
|
+
malformed.push(`${pointerLabel(at)}: ${problem}`);
|
|
191
|
+
}
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
for (const [key, item] of Object.entries(record))
|
|
195
|
+
findInclusions(item, `${at}/${pointerToken(key)}`, found, malformed);
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Satır içi gövdede `$file` açılmaz: dosya okuma yalnız bodyFile yolunda vardır. Açılmayan nesne platforma nesne
|
|
199
|
+
* olarak giderdi; istek gitmeden söylenir. `$file` anahtarı yoksa yalnız bir ağaç yürüyüşüdür, okuma yapılmaz.
|
|
200
|
+
*/
|
|
201
|
+
function assertNoInlineInclusions(tool, names, args) {
|
|
202
|
+
for (const name of names) {
|
|
203
|
+
const found = [];
|
|
204
|
+
const malformed = [];
|
|
205
|
+
findInclusions(args[name], '', found, malformed);
|
|
206
|
+
const sites = [...found.map(site => pointerLabel(site.at)), ...malformed];
|
|
207
|
+
if (sites.length > 0) {
|
|
208
|
+
throw new PlatformApiError(`${tool}: '${name}' içinde $file nesnesi var (${sites.join(' · ')}) — $file yalnız bodyFile gövdesinde açılır. Metni satır içinde doğrudan yazın ya da gövdeyi bodyFile ile (.json dosyası) verin. İstek gönderilmedi.`, 400);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function replaceInclusions(value, at, texts) {
|
|
213
|
+
const text = texts.get(at);
|
|
214
|
+
if (text !== undefined)
|
|
215
|
+
return text;
|
|
216
|
+
if (Array.isArray(value))
|
|
217
|
+
return value.map((item, index) => replaceInclusions(item, `${at}/${index}`, texts));
|
|
218
|
+
if (value && typeof value === 'object') {
|
|
219
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, replaceInclusions(item, `${at}/${pointerToken(key)}`, texts)]));
|
|
220
|
+
}
|
|
221
|
+
return value;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Eklenen metin bir kez okunur ve olduğu gibi konur: satır sonu korunur, metin yeniden JSON olarak ayrıştırılmaz —
|
|
225
|
+
* eklenen dosyanın içindeki `$file` bu yüzden metin kalır (iç içe ekleme yok). UTF-8 olmayan dosya sessizce � ile
|
|
226
|
+
* bozulacağına reddedilir; baştaki UTF-8 BOM'u çözücü düşürür (`bytes` onu yine sayar). Yer bilgisi (`at`) gövde argümanına göre JSON Pointer'dır.
|
|
227
|
+
*/
|
|
228
|
+
async function includeFiles(tool, value, folder, bodyBytes, roots, config) {
|
|
229
|
+
const sites = [];
|
|
230
|
+
const malformed = [];
|
|
231
|
+
findInclusions(value, '', sites, malformed);
|
|
232
|
+
if (malformed.length > 0) {
|
|
233
|
+
throw new PlatformApiError(`${tool}: bodyFile'da biçimi bozuk $file nesnesi — ${malformed.join(' · ')}. Ekleme yalnız tam { "$file": "<yol>" } biçimidir (tek anahtar, dizgi değer). İstek gönderilmedi.`, 400);
|
|
234
|
+
}
|
|
235
|
+
if (sites.length === 0)
|
|
236
|
+
return { value, files: [] };
|
|
237
|
+
if (sites.length > maxInclusions) {
|
|
238
|
+
throw new PlatformApiError(`${tool}: bodyFile ${sites.length} $file eklemesi taşıyor; sınır ${maxInclusions}. İlk fazlası: ${pointerLabel(sites[maxInclusions]?.at ?? '')} — gövdeyi birden çok çağrıya bölün. İstek gönderilmedi.`, 400);
|
|
239
|
+
}
|
|
240
|
+
const decoder = new TextDecoder('utf-8', { fatal: true });
|
|
241
|
+
const texts = new Map();
|
|
242
|
+
const files = [];
|
|
243
|
+
let total = bodyBytes;
|
|
244
|
+
for (const site of sites) {
|
|
245
|
+
const label = `$file (${pointerLabel(site.at)} → '${site.file}')`;
|
|
246
|
+
const read = await readGuardedFile(tool, label, resolve(folder, site.file), includedExtensions, maxIncludedFileBytes, roots, config);
|
|
247
|
+
total += read.buffer.length;
|
|
248
|
+
if (total > maxBodyFileBytes) {
|
|
249
|
+
throw new PlatformApiError(`${tool}: bodyFile ve $file eklemeleri birlikte 5 MB toplam gövde sınırını aşıyor (${pointerLabel(site.at)} eklemesinde). Tanımı parçalara bölün. İstek gönderilmedi.`, 400);
|
|
250
|
+
}
|
|
251
|
+
try {
|
|
252
|
+
texts.set(site.at, decoder.decode(read.buffer));
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
throw new PlatformApiError(`${tool}: ${label} UTF-8 değil — ${read.real}. Dosyayı UTF-8 olarak kaydedin. İstek gönderilmedi.`, 400);
|
|
256
|
+
}
|
|
257
|
+
files.push({ at: site.at, path: read.real, bytes: read.buffer.length });
|
|
101
258
|
}
|
|
102
|
-
return {
|
|
259
|
+
return { value: replaceInclusions(value, '', texts), files };
|
|
103
260
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { INTERNAL_ROLE_SEPARATOR } from '../../knowledge/componentCatalog.js';
|
|
2
|
+
import { AWAITING_WINDOW_SELECTOR } from './readiness.js';
|
|
2
3
|
import { studioWebTarget } from './targets/studio-web.js';
|
|
3
4
|
/**
|
|
4
5
|
* `swipe` jestinin adım sayısı ve aralığı: react-native-web yanıtlayıcısı tek seferde atılan hareketi jest saymaz
|
|
@@ -38,12 +39,23 @@ export function actionScript(action, captureTarget = studioWebTarget) {
|
|
|
38
39
|
return `(function () {
|
|
39
40
|
var count = performance.getEntriesByType('resource').length;
|
|
40
41
|
var node = document.querySelector(${selector});
|
|
41
|
-
if (!node)
|
|
42
|
+
if (!node) {
|
|
43
|
+
// g-11: açık önizleme penceresi (onay/uyarı/biyometri) sayfanın düğmesini örtüyor olabilir; not hangisi açık söyler.
|
|
44
|
+
var open = document.querySelector('${AWAITING_WINDOW_SELECTOR}');
|
|
45
|
+
return { found: false, applied: false, requests: count, openWindow: open ? { id: String(open.getAttribute('data-element-id')), kind: open.getAttribute('data-dialog-kind') } : null };
|
|
46
|
+
}
|
|
42
47
|
// g-04 sırası: düğümün kendisi basılabilirse o → <id>__press → içteki ilk basılabilir öğe.
|
|
43
48
|
// Mobil çoklu seçim tetikleyicisi kökte role="combobox" taşır ve içindeki ilk role="button"
|
|
44
49
|
// çipin "kaldır" düğmesidir — eskiden ona basılıp seçim siliniyordu (37 S-18). Tek basışlı
|
|
45
50
|
// denetimin yüzeyi <id>__press'tir (ui-native press-role-guard), anahtarınki role="switch".
|
|
46
51
|
var press = document.querySelector(${JSON.stringify(`[data-element-id="${String(action.target ?? '')}${INTERNAL_ROLE_SEPARATOR}press"]`)});
|
|
52
|
+
// g-11 incelemesi: önizleme penceresi (onay/uyarı/biyometri) açıkken altında kalan sayfa öğesine gerçek kullanıcı
|
|
53
|
+
// basamaz; DOM tıklaması basardı ve sayfa eylemi pencere yanıtlanmadan koşardı.
|
|
54
|
+
var shield = document.querySelector('${AWAITING_WINDOW_SELECTOR}');
|
|
55
|
+
if (shield && !shield.contains(node)) {
|
|
56
|
+
var shieldId = String(shield.getAttribute('data-element-id'));
|
|
57
|
+
return { found: true, applied: false, requests: count, openWindow: { id: shieldId, kind: shield.getAttribute('data-dialog-kind') }, note: "önizleme penceresi " + shieldId + " açık ve sayfayı örtüyor: gerçek kullanıcı alttaki öğeye basamaz, eylem uygulanmadı — önce pencereyi yanıtlayın (" + shieldId + "__confirm / __cancel, biyometride __verify / __cancel)" };
|
|
58
|
+
}
|
|
47
59
|
var target = ${pressSurface} || node.matches('button, a, [role="button"], [role="switch"], [role="combobox"]') ? node
|
|
48
60
|
: (press || node.querySelector('button, a, [role="button"], [role="switch"]') || node);
|
|
49
61
|
// S-2: sekme grubunun KÖKÜNE click, DOM sırasındaki ilk sekmeye gider ve "uygulandı" der —
|
|
@@ -433,6 +445,13 @@ export function actionScript(action, captureTarget = studioWebTarget) {
|
|
|
433
445
|
// K1 (g-04): gerçek kullanıcı alana odaklanır, yazar ve çıkar. Blur'suz yazımda odak kaybına
|
|
434
446
|
// bağlı akış (ui-native girişlerin 'changed'i) hiç koşmuyordu (37 S-19). focus() odağı alamazsa
|
|
435
447
|
// (görünmez alan, odaksız belge) olaylar elle atılır: Svelte 'blur'u, React 'focusout'u dinler.
|
|
448
|
+
// blur: false (g-12) telefonda klavye açıkken yazılan hâli sınar: odak alanda kalır, not söyler.
|
|
449
|
+
var keepFocus = ${JSON.stringify(action.blur === false)};
|
|
450
|
+
var done = function () {
|
|
451
|
+
var result = { found: true, applied: true, requests: count };
|
|
452
|
+
if (keepFocus) result.note = "blur: false — odak alanda kaldı; odak kaybına bağlı akış ('changed') bu adımda koşmaz";
|
|
453
|
+
return finish(result);
|
|
454
|
+
};
|
|
436
455
|
var focusField = function () {
|
|
437
456
|
field.focus();
|
|
438
457
|
if (document.activeElement === field) return;
|
|
@@ -448,15 +467,15 @@ export function actionScript(action, captureTarget = studioWebTarget) {
|
|
|
448
467
|
if (field instanceof HTMLInputElement && (field.type === 'checkbox' || field.type === 'radio')) {
|
|
449
468
|
var shouldCheck = wanted === 'true' || wanted === '1' || wanted === 'on';
|
|
450
469
|
if (field.checked !== shouldCheck) field.click();
|
|
451
|
-
blurField();
|
|
452
|
-
return
|
|
470
|
+
if (!keepFocus) blurField();
|
|
471
|
+
return done();
|
|
453
472
|
}
|
|
454
473
|
var setter = Object.getOwnPropertyDescriptor(proto, 'value').set;
|
|
455
474
|
setter.call(field, ${value});
|
|
456
475
|
field.dispatchEvent(new Event('input', { bubbles: true }));
|
|
457
476
|
field.dispatchEvent(new Event('change', { bubbles: true }));
|
|
458
|
-
blurField();
|
|
459
|
-
return
|
|
477
|
+
if (!keepFocus) blurField();
|
|
478
|
+
return done();
|
|
460
479
|
})()`;
|
|
461
480
|
}
|
|
462
481
|
/** `rf_range__start` → `rf_range`: iç rol hedefinin prop'ları kök düğümde yaşar. */
|