@appaflytech/zeyrek-ai-mcp 0.0.23 → 0.0.24
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/.env.example +15 -0
- package/dist/config.js +9 -0
- package/dist/index.js +2 -0
- package/dist/knowledge/componentCatalog.js +1 -1
- package/dist/knowledge/mobileComponentCatalog.js +231 -58
- package/dist/knowledge/mobileComponentEvents.js +118 -3
- package/dist/knowledge/normalize.js +16 -6
- package/dist/knowledge/platform/lint.js +110 -3
- package/dist/knowledge/platform/mobileRules.js +64 -8
- package/dist/knowledge/rules.js +56 -7
- package/dist/knowledge/styleResolve.js +49 -3
- package/dist/knowledge/styleResolveNative.js +104 -4
- package/dist/observability/trace.js +1 -1
- package/dist/platform/argumentAliases.js +128 -0
- package/dist/platform/bodyFile.js +103 -0
- package/dist/platform/capture/capture.js +29 -5
- package/dist/platform/capture/console-log.js +80 -0
- package/dist/platform/capture/index.js +1 -0
- package/dist/platform/diagnostics.js +10 -4
- package/dist/platform/http.js +23 -7
- package/dist/platform/responses.js +6 -2
- package/dist/platform/schemaReadMemory.js +60 -0
- package/dist/platform/secrets.js +9 -0
- package/dist/platform/strictArguments.js +68 -12
- package/dist/platform/tokenVault.js +187 -4
- package/dist/resources/guides/expressions.md +1 -1
- package/dist/resources/guides/microservice.md +9 -2
- package/dist/resources/guides/middleware.md +5 -1
- package/dist/resources/guides/mobile.md +150 -14
- package/dist/resources/guides/page.md +146 -39
- package/dist/resources/guides/theme.md +4 -1
- package/dist/resources/guides/workflow.md +7 -1
- package/dist/resources/schema-fingerprint.json +2 -2
- package/dist/server.js +1 -1
- package/dist/tools/datasources.js +1 -1
- package/dist/tools/designSchemaPatch.js +4 -3
- package/dist/tools/externalComponents.js +1 -1
- package/dist/tools/flows.js +70 -4
- package/dist/tools/layouts.js +5 -5
- package/dist/tools/microservices.js +26 -10
- package/dist/tools/pages.js +192 -36
- package/dist/tools/platformInfo.js +4 -1
- package/dist/tools/rows.js +7 -1
- package/dist/tools/schemaOps.js +1 -1
- package/dist/tools/screenWrite.js +4 -3
- package/dist/tools/screens.js +81 -0
- package/package.json +1 -1
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { maskSecretText } from '../secrets.js';
|
|
2
|
+
const bufferLimit = 20;
|
|
3
|
+
const reportLimit = 5;
|
|
4
|
+
const textLimit = 300;
|
|
5
|
+
const stackLines = 5;
|
|
6
|
+
const clip = (text) => (text.length > textLimit ? `${text.slice(0, textLimit - 1)}…` : text);
|
|
7
|
+
/**
|
|
8
|
+
* Çekim boyunca tarayıcı konsolunun hata/uyarı satırları ve yakalanmamış istisnalar. Koşuda sayfa
|
|
9
|
+
* çizilmediğinde `page_screenshot` yalnız "runtime hiç render olmadı" diyordu; nedeni bulmak için
|
|
10
|
+
* ajan kendi Playwright betiğini yazıp Studio paketinin sıkıştırılmış kaynağını okudu
|
|
11
|
+
* (plans/mobil-plantcare-acigi g-06). Aynı metin tekrarlanırsa sayaç artar, satır eklenmez; metin
|
|
12
|
+
* kırpılır ve kimlik parçaları maskelenir.
|
|
13
|
+
*/
|
|
14
|
+
export function createPageConsoleLog() {
|
|
15
|
+
const consoleRecords = [];
|
|
16
|
+
const pageErrorRecords = [];
|
|
17
|
+
return {
|
|
18
|
+
onConsole(message) {
|
|
19
|
+
const type = message.type();
|
|
20
|
+
const level = type === 'error' ? 'error' : type === 'warning' ? 'warning' : undefined;
|
|
21
|
+
if (!level)
|
|
22
|
+
return;
|
|
23
|
+
const text = clip(maskSecretText(message.text()));
|
|
24
|
+
const existing = consoleRecords.find(record => record.level === level && record.text === text);
|
|
25
|
+
if (existing)
|
|
26
|
+
existing.count += 1;
|
|
27
|
+
else if (consoleRecords.length < bufferLimit)
|
|
28
|
+
consoleRecords.push({ level, text, count: 1 });
|
|
29
|
+
},
|
|
30
|
+
onPageError(error) {
|
|
31
|
+
if (pageErrorRecords.length >= bufferLimit)
|
|
32
|
+
return;
|
|
33
|
+
const message = clip(maskSecretText(error instanceof Error ? `${error.name}: ${error.message}` : String(error)));
|
|
34
|
+
const stack = error instanceof Error && error.stack
|
|
35
|
+
? maskSecretText(error.stack.split('\n').slice(1, stackLines + 1).map(line => line.trim()).join('\n'))
|
|
36
|
+
: undefined;
|
|
37
|
+
if (!pageErrorRecords.some(record => record.message === message))
|
|
38
|
+
pageErrorRecords.push({ message, ...(stack ? { stack } : {}) });
|
|
39
|
+
},
|
|
40
|
+
/** Önce hatalar, sonra uyarılar; en çok beş kayıt. */
|
|
41
|
+
consoleErrors() {
|
|
42
|
+
return [...consoleRecords.filter(record => record.level === 'error'), ...consoleRecords.filter(record => record.level === 'warning')].slice(0, reportLimit);
|
|
43
|
+
},
|
|
44
|
+
pageErrors() {
|
|
45
|
+
return pageErrorRecords.slice(0, reportLimit);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const failureLimit = 3;
|
|
50
|
+
/**
|
|
51
|
+
* Çizilemeyen sayfanın hata metnine eklenen nedenler: çöken düğümler, yakalanmamış istisnalar, konsol
|
|
52
|
+
* hataları — bu sırayla, her birinden en çok üç; uyarılar eklenmez. Hiçbiri yoksa boş. Yalnız ilk neden
|
|
53
|
+
* yazılırken sayfa istisnası React'in bileşeni adlandıran konsol satırını gizliyordu.
|
|
54
|
+
*/
|
|
55
|
+
export function describePageFailure(log, renderErrors) {
|
|
56
|
+
const crashed = renderErrors.slice(0, failureLimit).map(record => `${record.elementId ?? '?'} — ${record.message}`);
|
|
57
|
+
const thrown = log.pageErrors().slice(0, failureLimit).map(record => `${record.message}${record.stack ? ` (${record.stack.split('\n')[0]})` : ''}`);
|
|
58
|
+
const logged = log.consoleErrors().filter(record => record.level === 'error').slice(0, failureLimit).map(record => record.text);
|
|
59
|
+
return [
|
|
60
|
+
crashed.length ? ` Çöken düğüm: ${crashed.join('; ')}.` : '',
|
|
61
|
+
thrown.length ? ` Sayfa istisnası: ${thrown.join('; ')}.` : '',
|
|
62
|
+
logged.length ? ` Konsol hatası: ${logged.join('; ')}.` : ''
|
|
63
|
+
].join('');
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Mobil çizicinin hata sınırlarının bıraktığı işaretler (`data-ng-render-error`). Düğümün dış sınırı
|
|
67
|
+
* (kancaları çöken düğüm) yer tutucuyu kendi kabuğunun dışında çizer ve kimliği işaretin üstünde taşır
|
|
68
|
+
* (`data-ng-render-error-id`); taşımıyorsa en yakın kabuk düğümdür.
|
|
69
|
+
*/
|
|
70
|
+
export const renderErrorsScript = "Array.from(document.querySelectorAll('[data-ng-render-error]')).slice(0, 20).map(function (el) { var own = el.getAttribute('data-ng-render-error-id'); var owner = own ? null : el.closest('[data-element-id]'); return { elementId: own || (owner ? owner.getAttribute('data-element-id') : undefined), message: el.getAttribute('data-ng-render-error') || '' }; })";
|
|
71
|
+
export function parseRenderErrors(value) {
|
|
72
|
+
if (!Array.isArray(value))
|
|
73
|
+
return [];
|
|
74
|
+
return value
|
|
75
|
+
.filter((entry) => entry !== null && typeof entry === 'object')
|
|
76
|
+
.map(entry => ({
|
|
77
|
+
...(typeof entry.elementId === 'string' ? { elementId: entry.elementId } : {}),
|
|
78
|
+
message: clip(maskSecretText(typeof entry.message === 'string' ? entry.message : ''))
|
|
79
|
+
}));
|
|
80
|
+
}
|
|
@@ -5,8 +5,9 @@ const MAX_NOTES = 8;
|
|
|
5
5
|
const MAX_NOTE_CHARS = 240;
|
|
6
6
|
const MAX_DROPS = 5;
|
|
7
7
|
export const diagnosticsParam = z.enum(['compact', 'full']).optional().describe("Yanıttaki gatewayNormalized / silentlyDropped listelerinin boyu. compact (varsayılan): aynı not tekilleşir, uzun not kısalır (en fazla 8); aynı yolun düşmeleri tek satırda sayıyla toplanır (en fazla 5 grup); droppedCount her zaman tam sayıdır. full: listelerin tamamı.");
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
/** `adjustable`: tool `diagnostics` parametresini alıyor mu — kısaltma notu yalnız alan tool'a onu önerir (g-08). */
|
|
9
|
+
export function runWithDiagnostics(mode, run, adjustable = true) {
|
|
10
|
+
return activeMode.run({ mode, adjustable }, run);
|
|
10
11
|
}
|
|
11
12
|
/**
|
|
12
13
|
* Tanılama listeleri her yazma çağrısında yanıta girer ve aynı gerekçe cümlesi çağrı başına tekrar
|
|
@@ -18,7 +19,7 @@ export function runWithDiagnostics(mode, run) {
|
|
|
18
19
|
* listeyi görür; kısaltma yalnız `diagnostics` parametresini taşıyan yazma tool'larında devreye girer.
|
|
19
20
|
*/
|
|
20
21
|
export function compactDiagnostics(payload) {
|
|
21
|
-
if (activeMode.getStore() !== 'compact')
|
|
22
|
+
if (activeMode.getStore()?.mode !== 'compact')
|
|
22
23
|
return payload;
|
|
23
24
|
if (!payload || typeof payload !== 'object' || Array.isArray(payload))
|
|
24
25
|
return payload;
|
|
@@ -58,7 +59,12 @@ export function compactDiagnostics(payload) {
|
|
|
58
59
|
}
|
|
59
60
|
if (hidden.length === 0)
|
|
60
61
|
return result;
|
|
61
|
-
return {
|
|
62
|
+
return {
|
|
63
|
+
...result,
|
|
64
|
+
diagnosticsNote: activeMode.getStore()?.adjustable === false
|
|
65
|
+
? 'Kısaltıldı; tam liste gateway günlüğünde (bu tool diagnostics parametresi almaz).'
|
|
66
|
+
: "Kısaltıldı; yazmayı tekrarlamayın. Tam liste: sonraki çağrıda diagnostics:'full'."
|
|
67
|
+
};
|
|
62
68
|
}
|
|
63
69
|
/**
|
|
64
70
|
* Aynı yolun farklı öğelerindeki düşmeler (`selectFields[fieldId=a].path`, `selectFields[fieldId=b].path`, …)
|
package/dist/platform/http.js
CHANGED
|
@@ -6,6 +6,20 @@ import { randomUUID } from 'node:crypto';
|
|
|
6
6
|
import { request } from 'undici';
|
|
7
7
|
export const requestAuthStorage = new AsyncLocalStorage();
|
|
8
8
|
const hiddenResponseHeaders = new Set(['set-cookie', 'authorization', 'proxy-authorization']);
|
|
9
|
+
/**
|
|
10
|
+
* g-08: yürütme yanıtının varsayılan başlıkları — işin okuduğu olanlar. `total-count` sayfalı tablo
|
|
11
|
+
* akışlarının sözleşmesidir (İK koşusu M-8), `location` yönlendirmenin, `x-request-id`/`traceparent`
|
|
12
|
+
* platform günlüğünde aynı isteği bulmanın anahtarıdır. Cloudflare ve sunucu başlıkları (`cf-ray`,
|
|
13
|
+
* `server`, `alt-svc`, `date` …) ajan bağlamında yer kaplıyordu ve hiçbir karar onlara bakmıyordu.
|
|
14
|
+
*/
|
|
15
|
+
const essentialResponseHeaders = new Set(['content-type', 'content-length', 'content-disposition', 'location', 'total-count', 'x-request-id', 'traceparent']);
|
|
16
|
+
export function headersForMode(headers, mode) {
|
|
17
|
+
if (mode === 'none')
|
|
18
|
+
return undefined;
|
|
19
|
+
if (mode === 'all')
|
|
20
|
+
return headers;
|
|
21
|
+
return Object.fromEntries(Object.entries(headers).filter(([name]) => essentialResponseHeaders.has(name)));
|
|
22
|
+
}
|
|
9
23
|
/**
|
|
10
24
|
* Sayfalı tablo akışları `Total-Count` header'ına bağlı ama execute ailesi yalnız gövdeyi
|
|
11
25
|
* döndürüyordu; sözleşme hiçbir tool'dan doğrulanamıyordu (İK koşusu M-8). Gizli header'lar
|
|
@@ -153,13 +167,15 @@ export class Platform {
|
|
|
153
167
|
body = JSON.stringify(options.body);
|
|
154
168
|
}
|
|
155
169
|
const startedAt = Date.now();
|
|
156
|
-
logStep('istek →', `${method} ${url.href} · ${api} api${options.environment ? ` · env=${options.environment}` : ''}${options.anonymous ? ' · ANONİM (Authorization header YOK)' : ''}${isRetry ? ' · 401 sonrası tekrar' : ''}`,
|
|
157
|
-
? [
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
?
|
|
161
|
-
: options.
|
|
162
|
-
|
|
170
|
+
logStep('istek →', `${method} ${url.href} · ${api} api${options.environment ? ` · env=${options.environment}` : ''}${options.anonymous ? ' · ANONİM (Authorization header YOK)' : ''}${isRetry ? ' · 401 sonrası tekrar' : ''}`, options.hideRequestBody === true
|
|
171
|
+
? ['gövde: (gizli — kimlik bilgisi taşıyor, günlüğe yazılmaz)']
|
|
172
|
+
: logsDetail() && (options.multipart || options.form || options.body !== undefined)
|
|
173
|
+
? [`gövde: ${renderJson(options.multipart
|
|
174
|
+
? describeMultipart(options.multipart)
|
|
175
|
+
: options.form
|
|
176
|
+
? Object.fromEntries(options.form)
|
|
177
|
+
: options.body)}`]
|
|
178
|
+
: []);
|
|
163
179
|
let statusCode;
|
|
164
180
|
let payload;
|
|
165
181
|
let responseBytes = 0;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { compactDiagnostics } from './diagnostics.js';
|
|
2
|
+
import { argumentNotes } from './argumentAliases.js';
|
|
3
|
+
import { tokenTtlMinutes } from './tokenVault.js';
|
|
2
4
|
import { logError, logStep } from '../observability/trace.js';
|
|
3
5
|
import { PlatformApiError, describeErrorBody } from './errors.js';
|
|
4
6
|
import { PolicyBlockError } from '../knowledge/policy.js';
|
|
@@ -162,8 +164,10 @@ export function toolResult(payload, config, expectFailure) {
|
|
|
162
164
|
...(runtimeErrors.length > 0 ? { runtimeErrors } : {}),
|
|
163
165
|
...(runtimeErrors.length > 0 && expectFailure === true ? { expectedFailure: true } : {}),
|
|
164
166
|
...(masked.length > 0 ? { gatewayMasked: masked } : {}),
|
|
165
|
-
...(Object.keys(tokenRefs).length > 0 ? { tokenRefs, tokenRefNote:
|
|
166
|
-
...(truncated ? { truncated: true, truncationNote: 'Liste, yanıt boyutu sınırı nedeniyle kırpıldı; sayfalama ya da daha dar filtre kullanın.' } : {})
|
|
167
|
+
...(Object.keys(tokenRefs).length > 0 ? { tokenRefs, tokenRefNote: `Maskelenen JWT'ler gateway içinde tutuldu; bu referansları page_screenshot / page_runtime_get 'asUser' alanına verebilirsiniz (${tokenTtlMinutes()} dk geçerli). Jetonun kendisi yanıta ve günlüğe hiç girmez.` } : {}),
|
|
168
|
+
...(truncated ? { truncated: true, truncationNote: 'Liste, yanıt boyutu sınırı nedeniyle kırpıldı; sayfalama ya da daha dar filtre kullanın.' } : {}),
|
|
169
|
+
...(argumentNotes().aliasesApplied.length > 0 ? { aliasesApplied: argumentNotes().aliasesApplied } : {}),
|
|
170
|
+
...(argumentNotes().bodyFile ? { bodyFile: argumentNotes().bodyFile } : {})
|
|
167
171
|
};
|
|
168
172
|
return {
|
|
169
173
|
content: [{ type: 'text', text: JSON.stringify(enriched) }],
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sayfa okumalarının belleği. İki soru cevaplar:
|
|
3
|
+
*
|
|
4
|
+
* - "Bu sayfa sürümü kırpık okundu mu" (ZM-28, PAGE-038): `page_schema_get(mode:'full',
|
|
5
|
+
* allowTruncated:true)` sığmayan şemayı kırparak döndüğünde anahtar işaretlenir; aynı sürümü
|
|
6
|
+
* `baseVersion` olarak veren `page_schema_patch` uyarı taşır — plan, düşen düğümleri görmeden
|
|
7
|
+
* yapılmış olabilir.
|
|
8
|
+
* - "Bu gateway sayfanın hangi sürümünü en son gördü" (PAGE-039, `plans/mobil-plantcare-acigi`
|
|
9
|
+
* g-05): `page_get`, `page_schema_get`, `page_schema_patch`'in `newVersion`'ı ve `page_update`'in
|
|
10
|
+
* kendi yazdığı sürüm hatırlanır; `page_update` `baseVersion` verilmediğinde onu gönderir. Hiç
|
|
11
|
+
* okunmamış sayfaya sürümsüz tam gövde yazılmaz.
|
|
12
|
+
*
|
|
13
|
+
* SÜREÇ İÇİNDEDİR ve yalnız anahtar/sürüm tutar (gövde yok); `CaptureMemory` ile aynı halka:
|
|
14
|
+
* yeniden yazılan anahtar sona gider, en eskisi düşer.
|
|
15
|
+
*/
|
|
16
|
+
export function schemaReadKey(project, environment, pageId, version) {
|
|
17
|
+
return `${project}/${environment}/${pageId}@${version}`;
|
|
18
|
+
}
|
|
19
|
+
export function pageReadKey(project, environment, pageId) {
|
|
20
|
+
return `${project}/${environment}/${pageId}`;
|
|
21
|
+
}
|
|
22
|
+
export class SchemaReadMemory {
|
|
23
|
+
limit;
|
|
24
|
+
truncated = new Set();
|
|
25
|
+
versions = new Map();
|
|
26
|
+
constructor(limit = 200) {
|
|
27
|
+
this.limit = limit;
|
|
28
|
+
}
|
|
29
|
+
rememberVersion(key, version) {
|
|
30
|
+
this.versions.delete(key);
|
|
31
|
+
this.versions.set(key, version);
|
|
32
|
+
while (this.versions.size > this.limit) {
|
|
33
|
+
const oldest = this.versions.keys().next().value;
|
|
34
|
+
if (oldest === undefined)
|
|
35
|
+
break;
|
|
36
|
+
this.versions.delete(oldest);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
lastVersion(key) {
|
|
40
|
+
return this.versions.get(key);
|
|
41
|
+
}
|
|
42
|
+
markTruncated(key) {
|
|
43
|
+
this.truncated.delete(key);
|
|
44
|
+
this.truncated.add(key);
|
|
45
|
+
while (this.truncated.size > this.limit) {
|
|
46
|
+
const oldest = this.truncated.values().next().value;
|
|
47
|
+
if (oldest === undefined)
|
|
48
|
+
break;
|
|
49
|
+
this.truncated.delete(oldest);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
wasTruncated(key) {
|
|
53
|
+
return this.truncated.has(key);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* SÜREÇ düzeyinde tek bellek: HTTP taşıması her POST için yeni bir gateway sunucusu kurduğundan
|
|
58
|
+
* (`sharedCaptureMemory` notu) okuma ile yazma ayrı `registerPageTools` çağrılarında koşar.
|
|
59
|
+
*/
|
|
60
|
+
export const sharedSchemaReadMemory = new SchemaReadMemory();
|
package/dist/platform/secrets.js
CHANGED
|
@@ -38,3 +38,12 @@ export function maskSecrets(payload) {
|
|
|
38
38
|
const tokenRefs = {};
|
|
39
39
|
return { payload: walk(payload, '', masked, tokenRefs, 0), masked, tokenRefs };
|
|
40
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Serbest METİNDEKİ kimlik parçaları (konsol satırı, hata yığını): JWT biçimli dize ve `Bearer …`.
|
|
43
|
+
* `maskSecrets` anahtar adına bakar; tarayıcı konsolu anahtarsız düz metin taşır (g-06).
|
|
44
|
+
*/
|
|
45
|
+
export function maskSecretText(text) {
|
|
46
|
+
return text
|
|
47
|
+
.replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, `Bearer ${maskedValue}`)
|
|
48
|
+
.replace(/\b[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g, maskedValue);
|
|
49
|
+
}
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { diagnosticsParam, runWithDiagnostics } from './diagnostics.js';
|
|
3
|
+
import { applyArgumentAliases, closestArgument, runWithArgumentNotes } from './argumentAliases.js';
|
|
4
|
+
import { BODY_FILE_ARGUMENTS, bodyFileParam, loadBodyFile } from './bodyFile.js';
|
|
5
|
+
import { PlatformApiError } from './errors.js';
|
|
6
|
+
import { logError } from '../observability/trace.js';
|
|
7
|
+
/** Doğrulanmış argümanlara takılan çeviri notları (`ArgumentNotes`); JSON'a ve platform isteğine girmez. */
|
|
8
|
+
const ARGUMENT_NOTES = Symbol('argumentNotes');
|
|
3
9
|
/**
|
|
4
10
|
* `writes`/`updates` açıklamalı ama KAYIT yazmayan tool'lar: çalıştırma, önizleme, dışa aktarma, indirme,
|
|
5
11
|
* bağlantı testi. Yanıtlarında yazma tanılaması (silentlyDropped) yoktur; parametre yalnız tool listesini
|
|
@@ -25,25 +31,75 @@ function isRawShape(value) {
|
|
|
25
31
|
* olmayan bir sorgu kusuruna yönlendirdi. Tek tek tool'lara alan eklemek sınıfı kapatmaz; sessizlik
|
|
26
32
|
* burada, SDK'ya şema verilmeden önce kaldırılır.
|
|
27
33
|
*/
|
|
28
|
-
export function rejectUnknownArguments(server) {
|
|
34
|
+
export function rejectUnknownArguments(server, platform) {
|
|
29
35
|
const registerTool = server.registerTool.bind(server);
|
|
36
|
+
// Zorunlu gövde argümanı `bodyFile` açıkken şemada isteğe bağlı yayınlanır (yoksa şemaya uyan istemci
|
|
37
|
+
// gövdeyi satır içinde yollamak zorunda kalır ve `bodyFile` "birlikte verilemez" ile reddedilirdi);
|
|
38
|
+
// zorunluluk dosya okunduktan sonra burada denetlenir.
|
|
39
|
+
const requiredBodies = new Map();
|
|
40
|
+
// g-07/g-08: `bodyFile` ve takma ad tablosu SDK'nın strict doğrulamasından ÖNCE koşmalı — sonra
|
|
41
|
+
// koşsa yanlış ad zaten reddedilmiş, dosyadan gelen gövde doğrulanmamış olur. SDK doğrulamayı
|
|
42
|
+
// `validateToolInput` içinde yapar; çeviri orada uygulanır ve notlar doğrulanmış argümanlara
|
|
43
|
+
// sembol anahtarla takılır.
|
|
44
|
+
if (platform) {
|
|
45
|
+
const validator = server;
|
|
46
|
+
const validate = validator.validateToolInput.bind(server);
|
|
47
|
+
validator.validateToolInput = async (tool, args, name) => {
|
|
48
|
+
try {
|
|
49
|
+
const loaded = await loadBodyFile(name, args, platform.config);
|
|
50
|
+
const requiredBody = requiredBodies.get(name);
|
|
51
|
+
if (requiredBody && loaded.args?.[requiredBody] === undefined) {
|
|
52
|
+
throw new PlatformApiError(`${name}: '${requiredBody}' zorunlu — satır içinde ya da bodyFile ile (.json dosyası) verin. İstek gönderilmedi.`, 400);
|
|
53
|
+
}
|
|
54
|
+
const { args: aliased, applied } = await applyArgumentAliases(name, loaded.args, { platform, shape: tool.inputSchema?.shape ?? {} });
|
|
55
|
+
const parsed = await validate(tool, aliased, name);
|
|
56
|
+
const notes = { aliasesApplied: applied, ...(loaded.note ? { bodyFile: loaded.note } : {}) };
|
|
57
|
+
return (applied.length > 0 || loaded.note) && parsed && typeof parsed === 'object' ? Object.assign(parsed, { [ARGUMENT_NOTES]: notes }) : parsed;
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
// Doğrulamadan önceki çeviri (dosya, takma ad) `handle`'ın dışında koşar: hata burada günlüğe yazılır.
|
|
61
|
+
if (error instanceof PlatformApiError)
|
|
62
|
+
logError(error);
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
30
67
|
server.registerTool = ((name, config, callback) => {
|
|
31
68
|
if (!isRawShape(config.inputSchema))
|
|
32
69
|
return registerTool(name, config, callback);
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
70
|
+
// Tanılama notları her tool'da compact döner (g-08: okuma ve çalıştırma yanıtında da 8 not +
|
|
71
|
+
// "N not daha"). `diagnostics` parametresi yalnız kayıt yazan tool'lara, microservice_execute'a ve
|
|
72
|
+
// *_preview'a açılır — ötekilerde tool listesini büyütmesin diye varsayılan kullanılır. Parametre
|
|
73
|
+
// handler'a ULAŞMAZ (gövdeyi `...rest` ile platforma geçiren tool'larda sızardı), yalnız
|
|
74
|
+
// toolResult'ın okuduğu bağlama konur.
|
|
75
|
+
const writesRecords = config.annotations?.readOnlyHint === false && config.annotations.destructiveHint !== true && !NON_WRITING_TOOLS.has(name);
|
|
76
|
+
const exposesDiagnostics = writesRecords || name === 'microservice_execute' || name.endsWith('_preview');
|
|
77
|
+
const bodyArgument = BODY_FILE_ARGUMENTS[name];
|
|
78
|
+
const bodySchema = bodyArgument ? config.inputSchema[bodyArgument] : undefined;
|
|
79
|
+
if (bodyArgument && bodySchema && !bodySchema.isOptional())
|
|
80
|
+
requiredBodies.set(name, bodyArgument);
|
|
81
|
+
const shape = {
|
|
82
|
+
...config.inputSchema,
|
|
83
|
+
...(bodyArgument && bodySchema ? { [bodyArgument]: bodySchema.optional(), bodyFile: bodyFileParam(bodyArgument) } : {}),
|
|
84
|
+
...(exposesDiagnostics ? { diagnostics: diagnosticsParam } : {})
|
|
85
|
+
};
|
|
86
|
+
const invoke = (args, extra) => {
|
|
87
|
+
const { diagnostics, ...rest } = exposesDiagnostics ? args ?? {} : { ...args, diagnostics: undefined };
|
|
88
|
+
return runWithDiagnostics(diagnostics === 'full' ? 'full' : 'compact', () => callback(exposesDiagnostics ? rest : args, extra), exposesDiagnostics);
|
|
89
|
+
};
|
|
90
|
+
const run = (args, extra) => {
|
|
91
|
+
const { [ARGUMENT_NOTES]: notes, ...rest } = args ?? {};
|
|
92
|
+
return runWithArgumentNotes(notes ?? { aliasesApplied: [] }, () => invoke(rest, extra));
|
|
93
|
+
};
|
|
43
94
|
const accepted = Object.keys(shape).join(', ');
|
|
95
|
+
const suggestions = (keys) => keys
|
|
96
|
+
.map(key => ({ key, near: closestArgument(key, Object.keys(shape)) }))
|
|
97
|
+
.filter(entry => entry.near !== undefined)
|
|
98
|
+
.map(entry => ` '${entry.key}' → '${entry.near}' mi demek istediniz?`)
|
|
99
|
+
.join('');
|
|
44
100
|
const inputSchema = z.object(shape, {
|
|
45
101
|
errorMap: (issue, context) => issue.code === 'unrecognized_keys'
|
|
46
|
-
? { message: `${name} şu üst düzey argümanları tanımıyor: ${issue.keys.map(key => `'${key}'`).join(', ')}. Tanınmayan alan sessizce yok sayılmaz — çağrı hiç gönderilmedi. Bu tool'un okuduğu alanlar: ${accepted}
|
|
102
|
+
? { message: `${name} şu üst düzey argümanları tanımıyor: ${issue.keys.map(key => `'${key}'`).join(', ')}. Tanınmayan alan sessizce yok sayılmaz — çağrı hiç gönderilmedi. Bu tool'un okuduğu alanlar: ${accepted}.${suggestions(issue.keys)}` }
|
|
47
103
|
: { message: context.defaultError }
|
|
48
104
|
}).strict();
|
|
49
105
|
return registerTool(name, { ...config, inputSchema }, run);
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { PlatformApiError } from './errors.js';
|
|
2
2
|
import { randomBytes } from 'node:crypto';
|
|
3
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
import { logStep } from '../observability/trace.js';
|
|
3
6
|
/**
|
|
4
7
|
* `page_screenshot`/`page_runtime_get` açıklaması "jetonu uygulamanın kendi giriş ucundan alın
|
|
5
8
|
* (`microservice_execute`)" diyor, ama gizli alan maskesi `access_token`'ı `***` yapıyor ve o yol
|
|
@@ -9,10 +12,19 @@ import { randomBytes } from 'node:crypto';
|
|
|
9
12
|
* Kasa bu çelişkiyi jetonu bağlama HİÇ sokmadan çözer: maskelenen JWT gateway sürecinde tutulur,
|
|
10
13
|
* yanıta yalnız kısa ömürlü bir referans (`tok_…`) döner ve kimlik isteyen tool'lar referansı
|
|
11
14
|
* çözer. Jeton ne yanıtta ne günlükte açık görünür.
|
|
15
|
+
*
|
|
16
|
+
* g-09 (`plans/mobil-plantcare-acigi`, K9): kasa süreç sınırını aşar. `.env` `MCP_STATE_DIR`
|
|
17
|
+
* verilmişse kayıtlar `token-vault.json`'a (0600) yazılır ve süreç açılışta onu okur; süre
|
|
18
|
+
* `MCP_TOKEN_TTL_MINUTES`'tan gelir (verilmezse eski 15 dk). Dosya YALNIZ lokal hedefte yazılır —
|
|
19
|
+
* uzak bir org'un jetonu diske inmez. Test kullanıcısının (`asUser: 'test:<ad>'`) jetonu da burada,
|
|
20
|
+
* etiketiyle tutulur; aynı kullanıcı TTL boyunca yeniden giriş yapmaz.
|
|
12
21
|
*/
|
|
13
|
-
const
|
|
22
|
+
const legacyTtlMinutes = 15;
|
|
14
23
|
const maxEntries = 64;
|
|
15
24
|
const vault = new Map();
|
|
25
|
+
let ttlMs = legacyTtlMinutes * 60 * 1000;
|
|
26
|
+
let vaultFile;
|
|
27
|
+
let warnedRemote = false;
|
|
16
28
|
export const tokenRefPattern = /^tok_[0-9a-f]{16}$/;
|
|
17
29
|
/** Üç base64url parçalı JWT — parola ya da API anahtarı bu şekle uymaz. */
|
|
18
30
|
export function looksLikeJwt(value) {
|
|
@@ -30,29 +42,132 @@ function sweep(now) {
|
|
|
30
42
|
vault.delete(oldest.value);
|
|
31
43
|
}
|
|
32
44
|
}
|
|
33
|
-
|
|
45
|
+
function isLocalTarget(baseUrl) {
|
|
46
|
+
try {
|
|
47
|
+
const host = new URL(baseUrl).hostname;
|
|
48
|
+
return host === 'localhost' || host === '127.0.0.1' || host === '[::1]' || host.endsWith('.localhost');
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Aynı dosyayı birden çok gateway süreci paylaşır (her Claude Code oturumu kendi stdio sürecini açar).
|
|
56
|
+
* Yazma "son yazan kazanır" olursa bir sürecin kaydı ötekinin yazmasıyla silinirdi: yazmadan önce ve
|
|
57
|
+
* bellekte olmayan referans istenince dosya okunur, süresi dolmamış yabancı kayıtlar belleğe katılır.
|
|
58
|
+
*/
|
|
59
|
+
function mergeFromFile() {
|
|
60
|
+
if (!vaultFile)
|
|
61
|
+
return;
|
|
62
|
+
try {
|
|
63
|
+
const stored = JSON.parse(readFileSync(vaultFile, 'utf8'));
|
|
64
|
+
const now = Date.now();
|
|
65
|
+
for (const [ref, entry] of Object.entries(stored.entries ?? {})) {
|
|
66
|
+
if (!vault.has(ref) && tokenRefPattern.test(ref) && typeof entry.token === 'string' && typeof entry.expires === 'number' && entry.expires > now) {
|
|
67
|
+
vault.set(ref, { token: entry.token, expires: entry.expires, ...(typeof entry.label === 'string' ? { label: entry.label } : {}) });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// dosya yok ya da okunamadı: bellekteki kasa geçerli
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function persist() {
|
|
76
|
+
if (!vaultFile)
|
|
77
|
+
return;
|
|
78
|
+
mergeFromFile();
|
|
79
|
+
sweep(Date.now());
|
|
80
|
+
try {
|
|
81
|
+
const temporary = `${vaultFile}.${process.pid}.tmp`;
|
|
82
|
+
mkdirSync(dirname(vaultFile), { recursive: true, mode: 0o700 });
|
|
83
|
+
writeFileSync(temporary, JSON.stringify({ entries: Object.fromEntries(vault) }), { mode: 0o600 });
|
|
84
|
+
renameSync(temporary, vaultFile);
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
logStep('jeton kasası ✕', `kasa dosyası yazılamadı (${vaultFile}) — jetonlar bu süreçte bellekte kalır`, [error instanceof Error ? error.message : String(error)]);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Süreç açılışında bir kez çağrılır (`index.ts`); testler yeniden başlamayı `clearTokenVault` +
|
|
92
|
+
* bu çağrıyla benzetir. Dosyadaki süresi dolmuş kayıtlar yüklenmez ve dosya onlarsız yeniden yazılır.
|
|
93
|
+
*/
|
|
94
|
+
export function configureTokenVault(config) {
|
|
95
|
+
ttlMs = (config.tokenTtlMinutes ?? legacyTtlMinutes) * 60 * 1000;
|
|
96
|
+
vaultFile = undefined;
|
|
97
|
+
if (!config.stateDirectory)
|
|
98
|
+
return;
|
|
99
|
+
if (!isLocalTarget(config.orgApiBaseUrl)) {
|
|
100
|
+
if (!warnedRemote) {
|
|
101
|
+
warnedRemote = true;
|
|
102
|
+
logStep('jeton kasası', `ORG_API_BASE_URL lokal değil — kasa dosyaya YAZILMAZ, jetonlar yalnız süreç belleğinde tutulur (MCP_STATE_DIR yok sayıldı)`);
|
|
103
|
+
}
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
vaultFile = join(config.stateDirectory, 'token-vault.json');
|
|
107
|
+
persist();
|
|
108
|
+
}
|
|
109
|
+
/** Kasanın geçerli ömrü (dakika) — yanıt notları sabit bir süre yazmasın. */
|
|
110
|
+
export const tokenTtlMinutes = () => Math.round(ttlMs / 60000);
|
|
111
|
+
export function rememberToken(token, label) {
|
|
34
112
|
const now = Date.now();
|
|
35
113
|
sweep(now);
|
|
36
114
|
for (const [ref, entry] of vault) {
|
|
37
115
|
if (entry.token === token) {
|
|
38
116
|
entry.expires = now + ttlMs;
|
|
117
|
+
if (label !== undefined)
|
|
118
|
+
entry.label = label;
|
|
119
|
+
persist();
|
|
39
120
|
return ref;
|
|
40
121
|
}
|
|
41
122
|
}
|
|
42
123
|
const ref = `tok_${randomBytes(8).toString('hex')}`;
|
|
43
|
-
vault.set(ref, { token, expires: now + ttlMs });
|
|
124
|
+
vault.set(ref, { token, expires: now + ttlMs, ...(label !== undefined ? { label } : {}) });
|
|
125
|
+
persist();
|
|
44
126
|
return ref;
|
|
45
127
|
}
|
|
46
128
|
export function resolveToken(reference) {
|
|
129
|
+
if (!vault.has(reference))
|
|
130
|
+
mergeFromFile();
|
|
47
131
|
const entry = vault.get(reference);
|
|
48
132
|
if (!entry)
|
|
49
133
|
return undefined;
|
|
50
134
|
if (entry.expires <= Date.now()) {
|
|
51
135
|
vault.delete(reference);
|
|
136
|
+
persist();
|
|
52
137
|
return undefined;
|
|
53
138
|
}
|
|
54
139
|
return entry.token;
|
|
55
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* Jetonun kendi süresi (`exp`) kasanın TTL'inden kısa olabilir: uygulamanın jetonu 15 dk yaşarken kasa
|
|
143
|
+
* 60 dk tutuyordu ve süresi dolmuş jetonla çekilen kare sessizce çıkış yapmış sayfayı gösterirdi.
|
|
144
|
+
* 30 sn pay bırakılır; `exp` okunamayan jeton geçerli sayılır (karar uygulamanındır).
|
|
145
|
+
*/
|
|
146
|
+
const jwtExpired = (token) => {
|
|
147
|
+
try {
|
|
148
|
+
const payload = JSON.parse(Buffer.from(token.split('.')[1] ?? '', 'base64url').toString('utf8'));
|
|
149
|
+
return typeof payload.exp === 'number' && payload.exp * 1000 <= Date.now() + 30_000;
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
function labeledToken(label) {
|
|
156
|
+
const find = () => {
|
|
157
|
+
const now = Date.now();
|
|
158
|
+
for (const entry of vault.values()) {
|
|
159
|
+
if (entry.label === label && entry.expires > now && !jwtExpired(entry.token))
|
|
160
|
+
return entry.token;
|
|
161
|
+
}
|
|
162
|
+
return undefined;
|
|
163
|
+
};
|
|
164
|
+
const found = find();
|
|
165
|
+
if (found || !vaultFile)
|
|
166
|
+
return found;
|
|
167
|
+
mergeFromFile();
|
|
168
|
+
return find();
|
|
169
|
+
}
|
|
170
|
+
/** Bellekteki kasayı boşaltır (süreç yeniden başlamasının benzetimi); dosyaya dokunmaz. */
|
|
56
171
|
export function clearTokenVault() {
|
|
57
172
|
vault.clear();
|
|
58
173
|
}
|
|
@@ -66,8 +181,76 @@ export function resolveIdentityToken(value) {
|
|
|
66
181
|
return value;
|
|
67
182
|
const token = resolveToken(value);
|
|
68
183
|
if (token === undefined) {
|
|
69
|
-
throw new PlatformApiError(`'${value}' referansı geçersiz ya da süresi dolmuş (
|
|
184
|
+
throw new PlatformApiError(`'${value}' referansı geçersiz ya da süresi dolmuş (${Math.round(ttlMs / 60000)} dk). Tercih edilen yol asUser: 'test:<ad>' — gateway kök .env'deki test kullanıcısıyla kendisi giriş yapar. Yoksa uygulamanın giriş ucunu microservice_execute ile yeniden çağırıp yanıttaki tokenRefs değerini kullanın.`, 400);
|
|
185
|
+
}
|
|
186
|
+
return token;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Giriş yanıtındaki erişim jetonu: yanıt birden çok JWT taşıyabilir (erişim + yenileme). Anahtarı
|
|
190
|
+
* `access` içeren önce, sonra `refresh` içermeyen, en son ilk bulunan — yenileme jetonuyla istek atmak
|
|
191
|
+
* her uçta 401 demekti.
|
|
192
|
+
*/
|
|
193
|
+
function accessJwt(value) {
|
|
194
|
+
const found = [];
|
|
195
|
+
const collect = (node, key, depth) => {
|
|
196
|
+
if (depth > 12)
|
|
197
|
+
return;
|
|
198
|
+
if (looksLikeJwt(node)) {
|
|
199
|
+
found.push({ key, token: node });
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (Array.isArray(node))
|
|
203
|
+
node.forEach(entry => collect(entry, key, depth + 1));
|
|
204
|
+
else if (node && typeof node === 'object')
|
|
205
|
+
Object.entries(node).forEach(([name, entry]) => collect(entry, name, depth + 1));
|
|
206
|
+
};
|
|
207
|
+
collect(value, '', 0);
|
|
208
|
+
return (found.find(entry => /access/i.test(entry.key)) ?? found.find(entry => !/refresh/i.test(entry.key)) ?? found[0])?.token;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* g-09: `asUser` üç şekil alır — ham JWT, `tok_…` referansı ve `test:<ad>`. Üçüncüsünde gateway kök
|
|
212
|
+
* `.env`'deki `MCP_TEST_USER_<AD>` satırının tarif ettiği giriş ucunu (uygulamanın kendi uç'u)
|
|
213
|
+
* anonim çağırır, yanıttaki ilk JWT'yi kasaya etiketiyle koyar ve TTL boyunca yeniden kullanır.
|
|
214
|
+
* Parola ne yanıta, ne günlüğe (`hideRequestBody`), ne hata metnine girer; satırın değeri hiçbir
|
|
215
|
+
* mesajda gösterilmez. AI `.env`'i düzenlemez: tanımsız kullanıcı, insanın ekleyeceği değişkenin
|
|
216
|
+
* adıyla reddedilir.
|
|
217
|
+
*/
|
|
218
|
+
export async function resolveIdentity(value, platform, project, environment) {
|
|
219
|
+
if (value === undefined || !value.startsWith('test:'))
|
|
220
|
+
return resolveIdentityToken(value);
|
|
221
|
+
const name = value.slice('test:'.length).trim().toLowerCase();
|
|
222
|
+
const variable = `MCP_TEST_USER_${name.toUpperCase()}`;
|
|
223
|
+
const label = `test:${name}@${platform.resolveProject(project)}/${environment ?? platform.config.defaultEnvironment}`;
|
|
224
|
+
const cached = labeledToken(label);
|
|
225
|
+
if (cached)
|
|
226
|
+
return cached;
|
|
227
|
+
const raw = platform.config.testUsers[name];
|
|
228
|
+
if (!raw) {
|
|
229
|
+
const defined = Object.keys(platform.config.testUsers);
|
|
230
|
+
throw new PlatformApiError(`Test kullanıcısı '${name}' tanımlı değil. Kök .env'e insan ekler: ${variable}={"application":"<uygulama slug>","microservice":"<giriş ucu slug>","body":{…giriş gövdesi…}} (biçim: docs/ortam.md §Test kullanıcıları); gateway yeniden başlatılır. AI .env'i düzenlemez. Tanımlı olanlar: ${defined.length > 0 ? defined.join(', ') : '(yok)'}.`, 400);
|
|
231
|
+
}
|
|
232
|
+
let login;
|
|
233
|
+
try {
|
|
234
|
+
login = JSON.parse(raw);
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
throw new PlatformApiError(`${variable} JSON olarak okunamadı (değer gösterilmez). Biçim: {"application":"…","microservice":"…","body":{…}} — docs/ortam.md §Test kullanıcıları.`, 400);
|
|
238
|
+
}
|
|
239
|
+
if (typeof login.application !== 'string' || typeof login.microservice !== 'string') {
|
|
240
|
+
throw new PlatformApiError(`${variable} 'application' ve 'microservice' alanlarını taşımalı (değer gösterilmez) — docs/ortam.md §Test kullanıcıları.`, 400);
|
|
241
|
+
}
|
|
242
|
+
const endpoint = `${login.application}/${login.microservice}`;
|
|
243
|
+
const response = await platform.tenantRaw(typeof login.method === 'string' ? login.method : 'POST', project, `/m/${endpoint}`, {
|
|
244
|
+
environment,
|
|
245
|
+
body: login.body,
|
|
246
|
+
anonymous: true,
|
|
247
|
+
hideRequestBody: true
|
|
248
|
+
});
|
|
249
|
+
const token = accessJwt(response.body);
|
|
250
|
+
if (!token) {
|
|
251
|
+
throw new PlatformApiError(`Test kullanıcısı '${name}' girişi jeton döndürmedi: ${endpoint} HTTP ${response.httpStatus}. Kök .env'deki ${variable} satırını (uç ve gövde) ve kullanıcının uygulamada var olduğunu kontrol edin; gövde ve kimlik bilgisi gösterilmez.`, 400);
|
|
70
252
|
}
|
|
253
|
+
rememberToken(token, label);
|
|
71
254
|
return token;
|
|
72
255
|
}
|
|
73
256
|
/**
|
|
@@ -99,7 +99,7 @@ dalından devam eder.
|
|
|
99
99
|
| **Komponent adları** | Her komponent adıyla doğrudan değişkendir: `specialties_table.rows = data` hem prop'u yazar hem UI'ı günceller (change event). Addaki `-` silinir: `tickets-table` → `ticketstable` |
|
|
100
100
|
| `state.page` / `state.app` | Kalıcı state (oku/yaz): `state.page.selectedId = row.id`. **Tanımlı değişkenler de buradadır:** `variable_create` ile yazılan her `key` sayfa düzeyinde `state.page.<key>`, uygulama düzeyinde `state.app.<key>` olur; `value` nesneyse alanlarına inilir (`state.page.MyTest.isActive`) |
|
|
101
101
|
| `Steps` | Adım çıktıları: `Steps.load_specialties` — anahtar adımın **ADIdır**, `stepId` değil (`setStorage(task.name, …)`); `Steps.<adım>.error` o adımın hatası — ama YALNIZ HTTP durum metnidir (401 için `'Unauthorized'`), ucun mesajı değil. `Steps.error` ise hata akışı için doldurulur (`onError` koşmadan önce `setStorage('error', …)` yapar) ama **YAPIŞKANDIR**: storage sayfa başına tek `FlowMachine`'de yaşar ve `'error'` anahtarı hiç temizlenmez, yani ilk hatadan sonra o sayfadaki her akışta dolu kalır — normal akışta `Steps.error` okuyan bir ifade bayat hatayı görür ve şekli `{ message, code, step: { id, name, type, data } }`'dir: kullanıcıya gösterilecek metin `Steps.error.message`'tır (`err.response.data.message`, boşsa durum metni). **Başarılı bir adımda `Steps.<adım>.data` gerçek bir `Array`'dir** — `.map` · `.filter` · `.concat` · `.length` çalışır. Adım HATA dönerse aynı alan ucun hata gövdesini taşır; aşağıdaki kutuya bakın |
|
|
102
|
-
| `self` · `item` · `Row` / `RowIndex` | Tetikleyen komponent (yazılabilir) · repeater/tablo satırı (yazmak satırı günceller) |
|
|
102
|
+
| `self` · `item` · `Row` / `RowIndex` | Tetikleyen komponent (yazılabilir) · repeater/tablo satırı (yazmak YALNIZ o satırı günceller). Satır AKIŞINDA (satırdaki düğümün olayı) ARC-121 düzeltmesini (2026-09-27) taşıyan runtime gerekir, eskisinde boştu — sürümden bağımsız yol `<liste>.selectedItem` / `.selectedIndex`. Tekrarlayıcının KENDİ `change`/`select` akışında satır bağlamı yoktur. `item.x = …` yazımı tekrarlayıcının `change` olayı olarak iletilir: `change`'e bağlı akış her yazımda koşar, o akış da `item`'a yazarsa döngü olur |
|
|
103
103
|
| `event` | `{name, data, source: {id, props}}` |
|
|
104
104
|
| `cookie` | `cookie.get(name)` / `cookie.set(name, value, {expiresInHours, path})` / `cookie.remove(name)` — tarayıcı cookie'si |
|
|
105
105
|
| `setComponentProperty(name, key, value)` / `getComponentProperty(name, key)` | fonksiyon alternatifi |
|
|
@@ -242,9 +242,16 @@ DTO'dan süzülmez (WF-026 — `platform_guide('workflow')` §Hata yolu). Sözle
|
|
|
242
242
|
- **Operatör sözlüğü kapalıdır:** `eq · neq · lt · lte · gt · gte · contains · icontains ·
|
|
243
243
|
in · notin · startswith · endswith · istartswith · iendswith · isnull · isnotnull`.
|
|
244
244
|
**`like` YOKTUR** — metin
|
|
245
|
-
araması `contains`/`icontains` iledir. Kritik tuzak:
|
|
245
|
+
araması `contains`/`icontains` iledir. Kritik tuzak: eski backend bilinmeyen operatörü 400 ile
|
|
246
246
|
reddetmez, **sessizce `eq`'e düşürür** (enum çözümlemesi unknown'da default'a düşer) — uç
|
|
247
|
-
testten geçer ama
|
|
247
|
+
testten geçer ama yanlış sonuç döner (koşuda `ne` böyle geçti). 2026-09-27'den sonraki backend
|
|
248
|
+
uç çağrısının `filters` parametresinde (JSON dizisi) 400 verir; workflow tablo adımının filtresi
|
|
249
|
+
hâlâ sessizce `eq`'e düşer. Ad büyük/küçük harfe duyarlıdır: `NEQ` geçersizdir. Gateway her iki
|
|
250
|
+
sürümde istek gitmeden keser: uç tanımında ve
|
|
251
|
+
workflow tablo adımında MS-012/WF-015 (blok), çalışma anında `row_list` / `row_export` filtresinde ve
|
|
252
|
+
`microservice_execute` / `microservice_debug`'ın `filters` parametresinde hata — sık yanlışlara öneriyle
|
|
253
|
+
(`ne`/`!=` → `neq`, `like` → `contains`, `=` → `eq`). Studio'nun filtre ARAYÜZÜNDEKİ adlar
|
|
254
|
+
(`notContains`, `between`…) ayrı bir sözlüktür; arayüz onları bu işleçlere çevirir.
|
|
248
255
|
- **Write operasyonunda (`create`/`update`) yanıt yalnız primary key'dir** — platform sonucu
|
|
249
256
|
PK'ya indirger ve `selectFields`'ı kaydederken sessizce boşaltır. Yanıtın başka alan da
|
|
250
257
|
dönmesi gerekiyorsa **`returnFields`** kullanılır (öğeler `selectFields` ile aynı şekildedir:
|