@appaflytech/zeyrek-ai-mcp 0.0.52 → 0.0.53
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/microservice.md +13 -1
- package/dist/resources/guides/mobile-publish.md +4 -2
- package/dist/resources/guides/mobile-steps.md +40 -3
- package/dist/resources/guides/mobile.md +38 -8
- package/dist/resources/guides/page.md +39 -8
- package/dist/resources/guides/translation.md +3 -1
- package/dist/resources/schema-fingerprint.json +2 -2
- package/dist/tools/applications.js +7 -2
- 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/mobile.js +56 -0
- package/dist/tools/mobileStore.js +1 -1
- package/dist/tools/pages.js +45 -11
- 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
|
@@ -6,7 +6,7 @@ import { PersistentScreenshotBrowser, persistentContextOpener, screenshotProfile
|
|
|
6
6
|
import { actionScript, activeTabScript, rootElementId, runtimePropsScript } from './actions.js';
|
|
7
7
|
import { domSummaryScript } from './summary.js';
|
|
8
8
|
import { createPageConsoleLog, describePageFailure, isExpressionEcho, parseRenderErrors, renderErrorsScript, splitExpressionErrors } from './console-log.js';
|
|
9
|
-
import { decideReadiness, describeUnready, parseReadiness, readinessScript, runtimePath } from './readiness.js';
|
|
9
|
+
import { awaitingUserNote, decideReadiness, describeUnready, parseAwaitingWindow, parseReadiness, readinessScript, runtimePath } from './readiness.js';
|
|
10
10
|
/** Runtime'ın Connect Data bağlantısı için bastığı sözde akış kimliği: hata bir akış adımından değil bağlı prop'un okumasından gelir. */
|
|
11
11
|
const CONNECT_DATA_FLOW_ID = 'connect-data';
|
|
12
12
|
/**
|
|
@@ -159,6 +159,9 @@ const resizeSettleBudgetMs = 3000;
|
|
|
159
159
|
* çağrıya ve ~3.5 dakikaya mal oldu). İkiden fazlası turu uzatır, bir şey kazandırmaz.
|
|
160
160
|
*/
|
|
161
161
|
const navigationAttempts = 2;
|
|
162
|
+
/** g-12 oturumları: depo durumu çerez (jeton dahil) taşıdığı için diske değil süreç belleğine yazılır. */
|
|
163
|
+
const sessionTtlMs = 30 * 60_000;
|
|
164
|
+
const maxSessions = 8;
|
|
162
165
|
/**
|
|
163
166
|
* Yalnız zaman aşımı yeniden denenir. `net::ERR_CONNECTION_REFUSED` / DNS gibi hızlı düşen
|
|
164
167
|
* hatalarda Studio ayakta değildir; tekrar yalnız bekleme süresini iki katına çıkarır ve
|
|
@@ -232,6 +235,7 @@ export class StudioScreenshotter {
|
|
|
232
235
|
queue = Promise.resolve();
|
|
233
236
|
pending = 0;
|
|
234
237
|
recycleNext = false;
|
|
238
|
+
sessions = new Map();
|
|
235
239
|
constructor(config, launcher) {
|
|
236
240
|
this.config = config;
|
|
237
241
|
this.launcher = launcher;
|
|
@@ -307,7 +311,7 @@ export class StudioScreenshotter {
|
|
|
307
311
|
* işaretlenir ve kare yine çekilir, çünkü bozuk/boş sayfayı görmek de bir doğrulamadır. Veri
|
|
308
312
|
* yanıtından sonra fontlar + kısa yerleşme beklemesi Svelte hydrate'ine alan bırakır.
|
|
309
313
|
*/
|
|
310
|
-
async runActions(page, request, consoleLog) {
|
|
314
|
+
async runActions(page, context, request, consoleLog) {
|
|
311
315
|
if (!request.actions || request.actions.length === 0)
|
|
312
316
|
return undefined;
|
|
313
317
|
const outcomes = [];
|
|
@@ -319,12 +323,23 @@ export class StudioScreenshotter {
|
|
|
319
323
|
}
|
|
320
324
|
const snapshotBefore = parseReadiness(await page.evaluate(readinessScript).catch(() => undefined));
|
|
321
325
|
const activityBefore = snapshotBefore?.activity ?? null;
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
326
|
+
// `network` (g-12) sayfada betik koşturmaz: bağlamın ağını değiştirir ve sayfanın gördüğü `navigator.onLine`'ı
|
|
327
|
+
// okur. Ağ açılınca koşan eşitleme akışları aşağıdaki aynı hazırlık beklemesiyle `flowRuns`'a yazılır.
|
|
328
|
+
const raw = action.action === 'network'
|
|
329
|
+
? context.setOffline === undefined
|
|
330
|
+
? { found: true, applied: false, note: 'ağ denetimi yok: tarayıcı bağlamı setOffline desteklemiyor — ağ DEĞİŞMEDİ', requests: 0 }
|
|
331
|
+
: await (async () => {
|
|
332
|
+
const count = await page.evaluate("performance.getEntriesByType('resource').length").catch(() => 0);
|
|
333
|
+
await context.setOffline?.(action.value === 'offline');
|
|
334
|
+
const online = await page.evaluate('navigator.onLine').catch(() => undefined);
|
|
335
|
+
return { found: true, applied: true, requests: typeof count === 'number' ? count : 0, ...(typeof online === 'boolean' ? { online } : {}) };
|
|
336
|
+
})()
|
|
337
|
+
: await page.evaluate(actionScript(action, request.target)).catch(error => ({
|
|
338
|
+
found: false,
|
|
339
|
+
applied: false,
|
|
340
|
+
note: error instanceof Error ? error.message : String(error),
|
|
341
|
+
requests: 0
|
|
342
|
+
}));
|
|
328
343
|
const before = (raw ?? {});
|
|
329
344
|
// Hedef bulunamadıysa ajan neyi yanlış yazdığını KAREDEN çıkaramaz. `__` taşıyan hedef bir
|
|
330
345
|
// iç rol denemesidir: bilinen roller sayılır. Kök hedefi bulunamadıysa sorun elementId'nin
|
|
@@ -335,6 +350,9 @@ export class StudioScreenshotter {
|
|
|
335
350
|
: action.target.includes(INTERNAL_ROLE_SEPARATOR)
|
|
336
351
|
? `'${action.target}' sayfada yok. Bu bir İÇ ROL hedefi: biçim <elementId>__<rol> ve bilinen roller — ${knownInternalRoles().join(' · ')}. Roller görünen metinden değil kayıttaki anahtardan türer (sekme key'i, kart id'si); hedef bileşenin rollerini component_get çıktısındaki internalRoles verir.`
|
|
337
352
|
: `'${action.target}' elementId'si sayfada yok. page_schema_get ile elementId'leri listeleyin; bir komponentin İÇ kontrolünü hedefliyorsanız <elementId>__<rol> biçimini kullanın (component_get çıktısındaki internalRoles).`;
|
|
353
|
+
const openWindow = parseAwaitingWindow(before.openWindow);
|
|
354
|
+
if (openWindow !== null)
|
|
355
|
+
before.note = `${before.note} Açık önizleme penceresi: ${openWindow.id}${openWindow.kind === null ? '' : ` (${openWindow.kind})`}; sayfanın düğmeleri onun altında kalır.`;
|
|
338
356
|
}
|
|
339
357
|
// Olay akışı uç çağırıyor: yerleşme beklemeden sonraki istek sayısını ölçmek "istek gitti mi"
|
|
340
358
|
// sorusunu cevaplar — koşuda kusuru kesinleştiren gözlem buydu.
|
|
@@ -376,9 +394,11 @@ export class StudioScreenshotter {
|
|
|
376
394
|
: undefined;
|
|
377
395
|
const propWritten = typeof propsAfter === 'string' ? propsAfter !== before.propsBefore : undefined;
|
|
378
396
|
const applied = before.applied === true && propWritten !== false;
|
|
397
|
+
// g-11: eylem bir önizleme penceresi açtıysa (onay, uyarı, biyometri) sıradaki eylemin neye basacağı burada söylenir.
|
|
398
|
+
const awaitingNote = readiness.state === 'awaiting-user' ? awaitingUserNote(readiness.snapshot?.awaitingWindow) : undefined;
|
|
379
399
|
const note = propWritten === false
|
|
380
400
|
? `değer DOM'a yazıldı ama '${rootElementId(String(action.target ?? ''))}' düğümünün runtime prop'ları DEĞİŞMEDİ — bileşen olayı kendi change yoluna çevirmedi ya da değer zaten aynıydı. Çok alanlı bileşende <elementId>__<rol> kutusunu hedefleyin (component_get → internalRoles).`
|
|
381
|
-
: before.note;
|
|
401
|
+
: [before.note, awaitingNote].filter((part) => part !== undefined && part !== '').join(' ') || undefined;
|
|
382
402
|
outcomes.push({
|
|
383
403
|
index,
|
|
384
404
|
action: String(action.action),
|
|
@@ -395,6 +415,7 @@ export class StudioScreenshotter {
|
|
|
395
415
|
...(before.optionsSeen === undefined ? {} : { optionsSeen: before.optionsSeen }),
|
|
396
416
|
...(before.waitedMs === undefined ? {} : { waitedMs: before.waitedMs }),
|
|
397
417
|
...(before.submitVia === undefined ? {} : { submitVia: before.submitVia }),
|
|
418
|
+
...(before.online === undefined ? {} : { online: before.online }),
|
|
398
419
|
pageState: readiness.state
|
|
399
420
|
});
|
|
400
421
|
}
|
|
@@ -491,7 +512,47 @@ export class StudioScreenshotter {
|
|
|
491
512
|
context = await browser.newContext(contextOptions);
|
|
492
513
|
}
|
|
493
514
|
logStep('tarayıcı →', `GET ${request.url} · viewport ${request.viewport.width}x${request.viewport.height}${request.fullPage ? ' · fullPage' : ''}${request.token ? '' : ' · gateway jetonu YOK'}${(request.cookies ?? []).length > 0 ? ` · caller çerezleri: ${(request.cookies ?? []).map(cookie => cookie.name).join(', ')}` : ''}${request.token || (request.cookies ?? []).length > 0 ? '' : ' · ANONİM (çerez YOK)'}${request.colorScheme ? ` · tema ${request.colorScheme}` : ''}${request.motion === 'play' ? ' · hareket oynar' : ''}${request.headers ? ` · başlık: ${Object.keys(request.headers).join(', ')}` : ''}`);
|
|
515
|
+
let sessionRestored;
|
|
516
|
+
// Oturum depoyu bir kez kaydeder: başarılı yolda yanıttan önce, hata yolunda `finally`'de. Yükleme yarıda kaldıysa
|
|
517
|
+
// (`setStorageState` önce siler, sonra yazar) yarı boş depo iyi kaydın üstüne yazılmaz (g-12 inceleme #6).
|
|
518
|
+
let sessionPending = request.session !== undefined;
|
|
519
|
+
// Okunamayan depo eski kaydı BIRAKMAZ: bırakılsaydı sonraki çekim aradaki yazmaları sessizce kaybeden eski depoyu
|
|
520
|
+
// `sessionRestored: true` ile yüklerdi (g-12 inceleme #5).
|
|
521
|
+
const saveSession = async () => {
|
|
522
|
+
sessionPending = false;
|
|
523
|
+
if (request.session === undefined || !context.storageState)
|
|
524
|
+
return true;
|
|
525
|
+
const state = await context.storageState().catch(() => undefined);
|
|
526
|
+
this.sessions.delete(request.session);
|
|
527
|
+
if (state === undefined)
|
|
528
|
+
return false;
|
|
529
|
+
this.sessions.set(request.session, { state, at: Date.now() });
|
|
530
|
+
for (const [key, entry] of this.sessions) {
|
|
531
|
+
if (Date.now() - entry.at > sessionTtlMs || this.sessions.size > maxSessions)
|
|
532
|
+
this.sessions.delete(key);
|
|
533
|
+
}
|
|
534
|
+
return true;
|
|
535
|
+
};
|
|
494
536
|
try {
|
|
537
|
+
// Oturum çerezlerden ÖNCE yüklenir: `setStorageState` mevcut depoyu siler, sonra yazılan jeton çerezi kalsın.
|
|
538
|
+
// Yeteneği olmayan bağlamda çağrı sessizce taze depoyla çekilmez (colorScheme'in kuralı).
|
|
539
|
+
if (request.session !== undefined) {
|
|
540
|
+
if (!context.storageState || !context.setStorageState) {
|
|
541
|
+
throw new PlatformApiError(`session='${request.session.split('|')[0]}' uygulanamadı: tarayıcı bağlamı depo durumunu okuyup yükleyemiyor (storageState/setStorageState). Kare taze depoyla çekilirdi ve bu sessizce yanlış olurdu.`);
|
|
542
|
+
}
|
|
543
|
+
const saved = this.sessions.get(request.session);
|
|
544
|
+
if (saved !== undefined && Date.now() - saved.at > sessionTtlMs)
|
|
545
|
+
this.sessions.delete(request.session);
|
|
546
|
+
sessionRestored = saved !== undefined && Date.now() - saved.at <= sessionTtlMs;
|
|
547
|
+
// İlk çağrı da boş depoyla başlar: kalıcı profilde önceki çekimlerden kalan köken anlık görüntüye girmesin (#8).
|
|
548
|
+
try {
|
|
549
|
+
await context.setStorageState(sessionRestored && saved !== undefined ? saved.state : { cookies: [], origins: [] });
|
|
550
|
+
}
|
|
551
|
+
catch (error) {
|
|
552
|
+
sessionPending = false;
|
|
553
|
+
throw error;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
495
556
|
const cookies = new Map();
|
|
496
557
|
if (request.token)
|
|
497
558
|
cookies.set('NG_AccessToken', request.token);
|
|
@@ -573,7 +634,7 @@ export class StudioScreenshotter {
|
|
|
573
634
|
: readiness.state === 'not-ready' ? false : undefined;
|
|
574
635
|
const finalHref = readiness.snapshot?.href;
|
|
575
636
|
const finalUrl = finalHref !== undefined && runtimePath(finalHref) !== runtimePath(request.url) ? finalHref : undefined;
|
|
576
|
-
const actions = await this.runActions(page, request, consoleLog);
|
|
637
|
+
const actions = await this.runActions(page, context, request, consoleLog);
|
|
577
638
|
// Ölçüm EYLEMLERDEN SONRA: eylem sekme değiştirir, filtre uygular, panel açar — içerik
|
|
578
639
|
// boyu o zaman kesinleşir. Eskiden ölçüm eylemlerden önceydi ve ikinci sekmede 40 satırlık
|
|
579
640
|
// tablo açan bir çekim viewport boyunda kalıyordu; `contentHeight` ilk sekmenin boyuydu
|
|
@@ -643,6 +704,7 @@ export class StudioScreenshotter {
|
|
|
643
704
|
if (runtimeErrors.length > 0) {
|
|
644
705
|
logStep('akış hatası ✕', `${runtimeErrors.length} akış adımı hata bıraktı — sayfa yine 200 döndü`, runtimeErrors.map(error => `${error.step?.name ?? '?'}${error.element ? ` → ${error.element}.${error.property ?? '?'}` : ''}: ${error.message ?? ''}`));
|
|
645
706
|
}
|
|
707
|
+
const sessionSaved = await saveSession();
|
|
646
708
|
return {
|
|
647
709
|
...(png === undefined ? {} : { png, mimeType: format === 'jpeg' ? 'image/jpeg' : 'image/png' }),
|
|
648
710
|
...(png === undefined ? {} : (size => size === undefined ? {} : { imageSize: size })(imageSize(png))),
|
|
@@ -653,6 +715,7 @@ export class StudioScreenshotter {
|
|
|
653
715
|
renderWaitTimedOut: apiStatus === undefined,
|
|
654
716
|
elapsedMs,
|
|
655
717
|
pageState: readiness.state,
|
|
718
|
+
...(readiness.state === 'awaiting-user' && readiness.snapshot?.awaitingWindow ? { awaitingWindow: readiness.snapshot.awaitingWindow } : {}),
|
|
656
719
|
readyAfterMs: readiness.waitedMs,
|
|
657
720
|
...(finalUrl === undefined ? {} : { finalUrl }),
|
|
658
721
|
...(measured === undefined ? {} : { contentHeight: measured }),
|
|
@@ -665,10 +728,16 @@ export class StudioScreenshotter {
|
|
|
665
728
|
...(pageErrors.length > 0 ? { pageErrors } : {}),
|
|
666
729
|
...(renderErrors.length > 0 ? { renderErrors } : {}),
|
|
667
730
|
...(flowsSettled === undefined ? {} : { flowsSettled }),
|
|
668
|
-
...(navigationRetried ? { navigationRetried: true } : {})
|
|
731
|
+
...(navigationRetried ? { navigationRetried: true } : {}),
|
|
732
|
+
...(sessionRestored === undefined ? {} : { sessionRestored }),
|
|
733
|
+
...(sessionSaved ? {} : { sessionSaved: false })
|
|
669
734
|
};
|
|
670
735
|
}
|
|
671
736
|
finally {
|
|
737
|
+
// Hata yolunda da depo kapanıştan ÖNCE alınır (kalıcı profil kapanışta depoyu siler): ağsız yazılmış kayıt
|
|
738
|
+
// çekim düşse de oturumda kalır. En yenisi sona yazılır; süresi dolan ve sekizi aşan en eski oturum düşer.
|
|
739
|
+
if (sessionPending)
|
|
740
|
+
await saveSession();
|
|
672
741
|
await context.close().catch(() => undefined);
|
|
673
742
|
// Zamanlayıcı ÖNCE temizlenir: eskiden her çekim yenisini kurup eskisini bırakıyordu ve
|
|
674
743
|
// kaçan zamanlayıcı sonraki çekimin ortasında tarayıcıyı kapatıyordu (S-3).
|
|
@@ -72,6 +72,8 @@ export class PersistentScreenshotBrowser {
|
|
|
72
72
|
* Tek çekimlik oturum. Eski taze bağlamın yalıtımı korunur: başlık ve init script'i bağlama
|
|
73
73
|
* değil sayfaya yazılır (sonraki çekime sızmaz — `impersonated-user-id` dahil), kapanışta
|
|
74
74
|
* çerezler ve gezilen kökenlerin depoları (localStorage, IndexedDB, …) diskteki profilden silinir.
|
|
75
|
+
* `session` (g-12) bu silmeyi atlamaz: depo kapanıştan önce okunur, sonraki çekimde yüklenir; yüklenen kökenler de
|
|
76
|
+
* kapanışta silinir. Ağ kesildiyse sayfalar kapandıktan SONRA geri açılır — bağlam sonraki çekimle paylaşılır.
|
|
75
77
|
*/
|
|
76
78
|
class PersistentContextSession {
|
|
77
79
|
context;
|
|
@@ -79,6 +81,8 @@ class PersistentContextSession {
|
|
|
79
81
|
initScripts = [];
|
|
80
82
|
headers;
|
|
81
83
|
pages = [];
|
|
84
|
+
restoredOrigins = new Set();
|
|
85
|
+
offline = false;
|
|
82
86
|
constructor(context, options) {
|
|
83
87
|
this.context = context;
|
|
84
88
|
this.options = options;
|
|
@@ -92,6 +96,20 @@ class PersistentContextSession {
|
|
|
92
96
|
async setExtraHTTPHeaders(headers) {
|
|
93
97
|
this.headers = headers;
|
|
94
98
|
}
|
|
99
|
+
async setOffline(offline) {
|
|
100
|
+
await this.context.setOffline(offline);
|
|
101
|
+
this.offline = offline;
|
|
102
|
+
}
|
|
103
|
+
storageState() {
|
|
104
|
+
return this.context.storageState({ indexedDB: true });
|
|
105
|
+
}
|
|
106
|
+
async setStorageState(state) {
|
|
107
|
+
for (const entry of state.origins ?? []) {
|
|
108
|
+
if (typeof entry.origin === 'string')
|
|
109
|
+
this.restoredOrigins.add(entry.origin);
|
|
110
|
+
}
|
|
111
|
+
await this.context.setStorageState(state);
|
|
112
|
+
}
|
|
95
113
|
async newPage() {
|
|
96
114
|
const page = await this.context.newPage();
|
|
97
115
|
this.pages.push(page);
|
|
@@ -104,7 +122,7 @@ class PersistentContextSession {
|
|
|
104
122
|
return page;
|
|
105
123
|
}
|
|
106
124
|
async close() {
|
|
107
|
-
const origins = new Set();
|
|
125
|
+
const origins = new Set(this.restoredOrigins);
|
|
108
126
|
for (const page of this.pages) {
|
|
109
127
|
try {
|
|
110
128
|
const origin = new URL(page.url()).origin;
|
|
@@ -126,5 +144,9 @@ class PersistentContextSession {
|
|
|
126
144
|
await this.context.clearCookies().catch(() => undefined);
|
|
127
145
|
for (const page of this.pages)
|
|
128
146
|
await page.close().catch(() => undefined);
|
|
147
|
+
// Ağ EN SON açılır: sayfa açıkken açılsaydı çevrimdışı uygulama `online` olayıyla kuyruğunu sunucuya boşaltırdı —
|
|
148
|
+
// salt okunur bir çekimden görünmez yazma, oturumla taşınan kuyrukta da çift kayıt (g-12 inceleme #4).
|
|
149
|
+
if (this.offline)
|
|
150
|
+
await this.context.setOffline(false).catch(() => undefined);
|
|
129
151
|
}
|
|
130
152
|
}
|
|
@@ -14,6 +14,13 @@
|
|
|
14
14
|
* (`captureUnready: true` teşhis kipi dışında). Bozuk sayfayı görmek istiyorsan hata mesajı
|
|
15
15
|
* neyin görüldüğünü zaten söyler.
|
|
16
16
|
*/
|
|
17
|
+
/**
|
|
18
|
+
* Önizlemenin kendi pencereleri — uygulama düğümü değil, Studio kabuğunun öğesi. Açıkken bir akış adımı kullanıcının
|
|
19
|
+
* yanıtını bekler: `biometric-auth` (kök ADR 0021, `@ng-apps/ui-native` `preview-biometric.tsx`) ve `confirm`/`alert`
|
|
20
|
+
* (Studio `components/dialog`, plans/defter-saha-bulgulari f-11; tür `data-dialog-kind`'da).
|
|
21
|
+
* Seçici yalnız çift tırnak taşır: betiklere tek tırnakla gömülür.
|
|
22
|
+
*/
|
|
23
|
+
export const AWAITING_WINDOW_SELECTOR = '[data-element-id="preview-biometric"], [data-element-id="preview-dialog"]';
|
|
17
24
|
export const readinessScript = `(function () { /* readinessSnapshot */
|
|
18
25
|
var activity = window.__ngAppsFlowActivity;
|
|
19
26
|
return {
|
|
@@ -22,11 +29,34 @@ export const readinessScript = `(function () { /* readinessSnapshot */
|
|
|
22
29
|
skeleton: !!document.querySelector('.view-detail > .bx--skeleton__placeholder'),
|
|
23
30
|
viewDetail: !!document.querySelector('.view-detail'),
|
|
24
31
|
errorState: !!document.querySelector('.view-detail__error, .view-detail .error-page'),
|
|
25
|
-
awaitingUser: !!document.querySelector('
|
|
32
|
+
awaitingUser: !!document.querySelector('${AWAITING_WINDOW_SELECTOR}'),
|
|
33
|
+
awaitingWindow: (function () {
|
|
34
|
+
var open = document.querySelector('${AWAITING_WINDOW_SELECTOR}');
|
|
35
|
+
return open ? { id: String(open.getAttribute('data-element-id')), kind: open.getAttribute('data-dialog-kind') } : null;
|
|
36
|
+
})(),
|
|
26
37
|
readyState: String(document.readyState),
|
|
27
38
|
href: String(location.href)
|
|
28
39
|
};
|
|
29
40
|
})()`;
|
|
41
|
+
/** Tarayıcıdan gelen pencere değeri; kimliği olmayan değer pencere sayılmaz. */
|
|
42
|
+
export function parseAwaitingWindow(raw) {
|
|
43
|
+
if (!raw || typeof raw !== 'object')
|
|
44
|
+
return null;
|
|
45
|
+
const { id, kind } = raw;
|
|
46
|
+
return typeof id === 'string' && id !== '' ? { id, kind: typeof kind === 'string' ? kind : null } : null;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* `awaiting-user` karesinin ve pencere açıkken bulunamayan `click` hedefinin notu: hangi pencere açık, dalı sürmek için hangi
|
|
50
|
+
* düğmeye basılır. Kimlikler çevrilmiş metinden bağımsızdır; production'da (cihaz, `/view`) bu pencereler yoktur.
|
|
51
|
+
*/
|
|
52
|
+
export function awaitingUserNote(window) {
|
|
53
|
+
if (window?.id === 'preview-dialog') {
|
|
54
|
+
return window.kind === 'alert'
|
|
55
|
+
? "Bir akış kullanıcı penceresini bekliyor: önizlemenin uyarı penceresi (alert adımı) açık. Kare pencereyi gösterir; kapatıp akışı sürdürmek için sonraki eylem { action: 'click', target: 'preview-dialog__confirm' }. Kimlik yalnız Studio önizlemesindedir; cihazda pencere işletim sisteminindir."
|
|
56
|
+
: "Bir akış kullanıcı penceresini bekliyor: önizlemenin onay penceresi (confirm adımı) açık. Kare pencereyi gösterir; onay dalı için sonraki eylem { action: 'click', target: 'preview-dialog__confirm' }, red dalı için 'preview-dialog__cancel'. Kimlikler yalnız Studio önizlemesindedir; cihazda pencere işletim sisteminindir.";
|
|
57
|
+
}
|
|
58
|
+
return "Bir akış kullanıcı penceresini bekliyor: önizlemenin biyometri penceresi açık (kök ADR 0021). Kare pencereyi gösterir; dalı sürmek için sonraki eylem { action: 'click', target: 'preview-biometric__verify' } (Doğrulandı say) ya da 'preview-biometric__cancel' (vazgeç). Production /view'da pencere yoktur.";
|
|
59
|
+
}
|
|
30
60
|
/** Okunan değer gerçekten sayaç mı; değilse sayfa sayaç yayınlamıyordur (eski runtime). */
|
|
31
61
|
export function isFlowActivity(activity) {
|
|
32
62
|
if (!activity || typeof activity !== 'object')
|
|
@@ -48,6 +78,7 @@ export function parseReadiness(raw) {
|
|
|
48
78
|
viewDetail: value.viewDetail === true,
|
|
49
79
|
errorState: value.errorState === true,
|
|
50
80
|
awaitingUser: value.awaitingUser === true,
|
|
81
|
+
awaitingWindow: parseAwaitingWindow(value.awaitingWindow),
|
|
51
82
|
readyState: String(value.readyState ?? ''),
|
|
52
83
|
href: value.href
|
|
53
84
|
};
|
package/dist/platform/errors.js
CHANGED
|
@@ -12,6 +12,12 @@ export class PlatformApiError extends Error {
|
|
|
12
12
|
* izin slug'ları geçer ('entity.reveal grant'i'); `accessDecision` izni ondan okursa yanlış izin yazar.
|
|
13
13
|
*/
|
|
14
14
|
apiMessage;
|
|
15
|
+
/**
|
|
16
|
+
* Ağ hatası geçici: ad çözümleme ya da yanıt öncesi bağlantı kopması (D-19). Ajan tekrar edilebilirliği metni
|
|
17
|
+
* ayrıştırmadan bilsin diye araç hata yanıtına `transient: true` olarak çıkar. Zaman aşımı geçici SAYILMAZ:
|
|
18
|
+
* uzun işlemi tekrar etmek aynı süreyi bir kez daha bekletir.
|
|
19
|
+
*/
|
|
20
|
+
transient;
|
|
15
21
|
constructor(message, status, code) {
|
|
16
22
|
super(message);
|
|
17
23
|
this.status = status;
|
|
@@ -474,17 +480,51 @@ export function mapPlatformError(status, payload, context) {
|
|
|
474
480
|
}
|
|
475
481
|
const CONNECTION_RESET_CODES = new Set(['ECONNRESET', 'UND_ERR_SOCKET', 'EPIPE', 'ECONNABORTED']);
|
|
476
482
|
/**
|
|
477
|
-
*
|
|
478
|
-
*
|
|
483
|
+
* Ad çözümlemesi bağlantı açılmadan düşer; istek sunucuya hiç gitmemiştir. Defter uzun betiklerin ortasında `ENOTFOUND`
|
|
484
|
+
* gördü ve kendi istemcisine üç denemelik sarmal yazdı (D-19).
|
|
479
485
|
*/
|
|
480
|
-
|
|
486
|
+
const NAME_RESOLUTION_CODES = new Set(['ENOTFOUND', 'EAI_AGAIN']);
|
|
487
|
+
function transientNetworkKind(error) {
|
|
481
488
|
const failure = error;
|
|
482
|
-
|
|
489
|
+
const codes = [String(failure?.code), String(failure?.cause?.code)];
|
|
490
|
+
if (codes.some(code => NAME_RESOLUTION_CODES.has(code)))
|
|
491
|
+
return 'name-resolution';
|
|
492
|
+
if (codes.some(code => CONNECTION_RESET_CODES.has(code)))
|
|
493
|
+
return 'connection-reset';
|
|
494
|
+
return undefined;
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Tekrarı zararsız istekte (GET/HEAD, kimlik jetonu) geçici ağ hatasından sonraki beklemeler; uzunluğu tekrar sayısıdır
|
|
498
|
+
* (D-19: en çok iki tekrar). Zaman aşımı bu yola girmez — varsayılan istek süresi 100 sn, tekrar onu bir kez daha bekletirdi.
|
|
499
|
+
*/
|
|
500
|
+
export const NETWORK_RETRY_DELAYS_MS = [250, 1000];
|
|
501
|
+
/**
|
|
502
|
+
* Yanıt gelmeden düşen ve kısa beklemeyle geçebilen ağ hatası: ad çözümleme (`ENOTFOUND`/`EAI_AGAIN`) ya da yolda kopan
|
|
503
|
+
* bağlantı. Docker Desktop'ın host port yönlendirmesi yük altında açık bağlantıları ara ara düşürür — konteyner içinden
|
|
504
|
+
* aynı uç kopmaz (f-02, 2026-10-24 ölçümü: içeride 0/1800, host'ta `:3000` 4/1800, `:80` 2/1800). `undici` kodu kimi
|
|
505
|
+
* zaman `cause.code`'da taşır.
|
|
506
|
+
*/
|
|
507
|
+
export function isTransientNetworkError(error) {
|
|
508
|
+
return transientNetworkKind(error) !== undefined;
|
|
483
509
|
}
|
|
484
|
-
export function mapNetworkError(error, context) {
|
|
510
|
+
export function mapNetworkError(error, context, attempt = { attempts: 1, readOnly: context.method === 'GET' || context.method === 'HEAD' }) {
|
|
485
511
|
const cause = error instanceof Error ? error : new Error(String(error));
|
|
486
512
|
if (cause.name === 'TimeoutError' || cause.name === 'AbortError') {
|
|
487
513
|
return new PlatformApiError(`${context.api} API zaman aşımına uğradı (${context.method} ${context.path}). Uzun süren bir işlemse durumu ilgili *_history/*_status tool'undan izleyin.`);
|
|
488
514
|
}
|
|
515
|
+
const kind = transientNetworkKind(error);
|
|
516
|
+
const tried = attempt.attempts > 1 ? ` (${attempt.attempts} deneme)` : '';
|
|
517
|
+
if (kind === 'name-resolution') {
|
|
518
|
+
const failure = new PlatformApiError(`${context.api} API'nin adı çözülemedi (${context.baseUrl})${tried}: ${cause.message}. İstek sunucuya ULAŞMADI (ad çözülemedi) — aynı çağrıyı tekrarlamak güvenli. Sürüyorsa DNS/ağ bağlantısını kontrol edin.`);
|
|
519
|
+
failure.transient = true;
|
|
520
|
+
return failure;
|
|
521
|
+
}
|
|
522
|
+
if (kind === 'connection-reset') {
|
|
523
|
+
const failure = new PlatformApiError(attempt.readOnly
|
|
524
|
+
? `${context.api} API bağlantısı yanıt gelmeden koptu (${context.method} ${context.path})${tried}: ${cause.message}. Okuma isteği — tekrarlamak güvenli.`
|
|
525
|
+
: `${context.api} API bağlantısı yanıt gelmeden koptu (${context.method} ${context.path}): ${cause.message}. İstek sunucuya ulaşmış olabilir — tekrarlamadan önce yazının gerçekleşip gerçekleşmediğini ilgili *_get/*_list tool'uyla doğrulayın; yeniden yazmak çift kayıt üretebilir.`);
|
|
526
|
+
failure.transient = true;
|
|
527
|
+
return failure;
|
|
528
|
+
}
|
|
489
529
|
return new PlatformApiError(`${context.api} API'ye ulaşılamadı (${context.baseUrl}): ${cause.message}. API ayakta mı? Lokal ortam için docker compose ya da dotnet run kontrol edin.`);
|
|
490
530
|
}
|
package/dist/platform/http.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PlatformApiError,
|
|
1
|
+
import { NETWORK_RETRY_DELAYS_MS, PlatformApiError, isTransientNetworkError, mapNetworkError, mapPlatformError } from './errors.js';
|
|
2
2
|
import { logStep, logsDetail, renderJson } from '../observability/trace.js';
|
|
3
3
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
4
4
|
import { TokenProvider } from './token.js';
|
|
@@ -30,6 +30,9 @@ export function responseHeadersForTool(raw) {
|
|
|
30
30
|
.map(([name, value]) => [name.toLowerCase(), Array.isArray(value) ? value.join(', ') : value])
|
|
31
31
|
.filter((entry) => typeof entry[1] === 'string' && !hiddenResponseHeaders.has(entry[0])));
|
|
32
32
|
}
|
|
33
|
+
function sleep(ms) {
|
|
34
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
35
|
+
}
|
|
33
36
|
function buildMultipart(parts, boundary) {
|
|
34
37
|
const chunks = [];
|
|
35
38
|
for (const part of parts) {
|
|
@@ -74,10 +77,12 @@ function appendQuery(url, query) {
|
|
|
74
77
|
}
|
|
75
78
|
export class Platform {
|
|
76
79
|
config;
|
|
80
|
+
wait;
|
|
77
81
|
tokens;
|
|
78
|
-
constructor(config, tokens) {
|
|
82
|
+
constructor(config, tokens, wait = sleep) {
|
|
79
83
|
this.config = config;
|
|
80
|
-
this.
|
|
84
|
+
this.wait = wait;
|
|
85
|
+
this.tokens = tokens ?? new TokenProvider(config, wait);
|
|
81
86
|
}
|
|
82
87
|
resolveProject(project) {
|
|
83
88
|
const resolved = project ?? this.config.defaultProject;
|
|
@@ -134,7 +139,7 @@ export class Platform {
|
|
|
134
139
|
}
|
|
135
140
|
return `Bearer ${await this.tokens.getToken()}`;
|
|
136
141
|
}
|
|
137
|
-
async send(api, method, path, options, raw = false, isRetry = false,
|
|
142
|
+
async send(api, method, path, options, raw = false, isRetry = false, networkAttempt = 1) {
|
|
138
143
|
const baseUrl = api === 'org' ? this.config.orgApiBaseUrl : this.config.tenantApiBaseUrl;
|
|
139
144
|
const context = { api, method, path, baseUrl, environment: options.environment, authMode: this.config.authMode };
|
|
140
145
|
const url = new URL(`${baseUrl}${path}`);
|
|
@@ -207,12 +212,15 @@ export class Platform {
|
|
|
207
212
|
}
|
|
208
213
|
catch (error) {
|
|
209
214
|
logStep('ağ ✕', `${method} ${url.href} · ${Date.now() - startedAt} ms sonra ulaşılamadı`, [error instanceof Error ? `${error.name}: ${error.message}` : String(error)]);
|
|
210
|
-
// Yalnız tekrarı zararsız istek (GET/HEAD)
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
215
|
+
// Yalnız tekrarı zararsız istek (GET/HEAD): yazan istek sunucuya ulaşmış olabilir ve kimliksiz yazma çift kayıt üretir (D-19).
|
|
216
|
+
const readOnly = (method === 'GET' || method === 'HEAD') && !options.noNetworkRetry;
|
|
217
|
+
const delay = NETWORK_RETRY_DELAYS_MS[networkAttempt - 1];
|
|
218
|
+
if (readOnly && delay !== undefined && isTransientNetworkError(error)) {
|
|
219
|
+
logStep('ağ ⟳', `${method} ${url.href} · geçici ağ hatası, ${delay} ms sonra yeniden deneniyor (deneme ${networkAttempt + 1}/${NETWORK_RETRY_DELAYS_MS.length + 1})`);
|
|
220
|
+
await this.wait(delay);
|
|
221
|
+
return this.send(api, method, path, options, raw, isRetry, networkAttempt + 1);
|
|
214
222
|
}
|
|
215
|
-
throw mapNetworkError(error, context);
|
|
223
|
+
throw mapNetworkError(error, context, { attempts: networkAttempt, readOnly });
|
|
216
224
|
}
|
|
217
225
|
logStep('yanıt ←', `HTTP ${statusCode} · ${Date.now() - startedAt} ms · ${responseBytes} B`, statusCode >= 400 || logsDetail()
|
|
218
226
|
? [`gövde: ${options.expectBinary && statusCode < 400 ? `<ikili içerik · ${responseBytes} B>` : renderJson(payload)}`]
|
|
@@ -211,9 +211,15 @@ export function toolError(error, expectFailure) {
|
|
|
211
211
|
// Kasıtlı negatif testte (bayat version 409, yetkisiz kişi 403) platform hatası da bir KANITTIR:
|
|
212
212
|
// ölçüm aracı bu satırı plansız hata saymasın diye işaret taşınır.
|
|
213
213
|
const isExpectedPlatformError = expectFailure === true && (error instanceof PlatformApiError || error instanceof PolicyBlockError);
|
|
214
|
+
// Ajan tekrar edilebilir ağ hatasını metni ayrıştırmadan bilsin (D-19); yazan istekte metin tekrarın güvenli olup olmadığını söyler.
|
|
215
|
+
const isTransient = error instanceof PlatformApiError && error.transient === true;
|
|
216
|
+
const structuredContent = {
|
|
217
|
+
...(isExpectedPlatformError ? { expectedFailure: true } : {}),
|
|
218
|
+
...(isTransient ? { transient: true } : {})
|
|
219
|
+
};
|
|
214
220
|
return {
|
|
215
221
|
content: [{ type: 'text', text: message }],
|
|
216
|
-
...(
|
|
222
|
+
...(Object.keys(structuredContent).length > 0 ? { structuredContent } : {}),
|
|
217
223
|
isError: true
|
|
218
224
|
};
|
|
219
225
|
}
|
package/dist/platform/secrets.js
CHANGED
|
@@ -27,6 +27,12 @@ const maxDepth = 12;
|
|
|
27
27
|
* `StepCustomParameterDto {name,value,order}`, `QueryParameter {name,type,value}`: değer bağlama
|
|
28
28
|
* ifadesidir (`input.token`), maskelenirse get → update turunda `***` geri yazılır (GW-001).
|
|
29
29
|
* `expression` + `value` hiçbir paketli şemada geçmez, o yüzden bu ayrımın dışındadır.
|
|
30
|
+
*
|
|
31
|
+
* `expiresInHours` taşıyan ve değeri bir ifade olan satır da tanımdır (g-17): `setcookie` adımının `args`'ı
|
|
32
|
+
* `{ name: 'APP_Token', value: 'Steps.sign_in.data.token', expiresInHours }` — maskelenince flow_get → flow_update
|
|
33
|
+
* turu çereze `***` yazıp girişi bozuyordu. Değer tırnakla başlıyorsa dizgi sabitidir ve maskelenir: yürütme yanıtının
|
|
34
|
+
* `uiActions`'ı aynı şekli GERÇEK değerle taşır (`SetCookieStep.cs`: `value: "\"<değer>\""`), tanımdaki sabit de
|
|
35
|
+
* gömülü bir sırdır. `path` muafiyet değildir: `{ name, value, path }` gerçek bir çerezin serileşmiş hâlidir.
|
|
30
36
|
*/
|
|
31
37
|
export function secretPairName(record) {
|
|
32
38
|
if (!('value' in record))
|
|
@@ -35,6 +41,8 @@ export function secretPairName(record) {
|
|
|
35
41
|
return record.expression;
|
|
36
42
|
if ('type' in record || 'order' in record)
|
|
37
43
|
return undefined;
|
|
44
|
+
if ('expiresInHours' in record && typeof record.value === 'string' && !/^\s*["'`]/.test(record.value))
|
|
45
|
+
return undefined;
|
|
38
46
|
if (typeof record.name === 'string')
|
|
39
47
|
return record.name;
|
|
40
48
|
return typeof record.key === 'string' ? record.key : undefined;
|
package/dist/platform/token.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PlatformApiError, mapNetworkError, mapPlatformError } from './errors.js';
|
|
1
|
+
import { NETWORK_RETRY_DELAYS_MS, PlatformApiError, isTransientNetworkError, mapNetworkError, mapPlatformError } from './errors.js';
|
|
2
2
|
import { logStep } from '../observability/trace.js';
|
|
3
3
|
import { request } from 'undici';
|
|
4
4
|
function readExpiry(token) {
|
|
@@ -17,9 +17,11 @@ function readExpiry(token) {
|
|
|
17
17
|
}
|
|
18
18
|
export class TokenProvider {
|
|
19
19
|
config;
|
|
20
|
+
wait;
|
|
20
21
|
cached;
|
|
21
|
-
constructor(config) {
|
|
22
|
+
constructor(config, wait = ms => new Promise(resolve => setTimeout(resolve, ms))) {
|
|
22
23
|
this.config = config;
|
|
24
|
+
this.wait = wait;
|
|
23
25
|
}
|
|
24
26
|
async getToken() {
|
|
25
27
|
if (this.config.token)
|
|
@@ -40,29 +42,38 @@ export class TokenProvider {
|
|
|
40
42
|
let statusCode;
|
|
41
43
|
let payload;
|
|
42
44
|
logStep('kimlik →', `önbellekte geçerli token yok — POST ${this.config.orgApiBaseUrl}/connect/token · kullanıcı ${this.config.username ?? '(yok)'}`);
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
headers: {
|
|
47
|
-
host: `${this.config.organization}.${this.config.hostSuffix}`,
|
|
48
|
-
'content-type': 'application/x-www-form-urlencoded',
|
|
49
|
-
accept: 'application/json'
|
|
50
|
-
},
|
|
51
|
-
body: form.toString(),
|
|
52
|
-
signal: AbortSignal.timeout(this.config.requestTimeoutMs)
|
|
53
|
-
});
|
|
54
|
-
statusCode = response.statusCode;
|
|
55
|
-
const text = await response.body.text();
|
|
45
|
+
// Jeton isteği kayıt yazmaz: geçici ağ hatasında GET gibi yeniden denenir ve hata tekrarın güvenli olduğunu söyler
|
|
46
|
+
// (D-19; inceleme: soğuk jeton önbelleğinde tek DNS kesintisi okuyan aracı da düşürüyordu).
|
|
47
|
+
for (let attempt = 1;; attempt += 1) {
|
|
56
48
|
try {
|
|
57
|
-
|
|
49
|
+
const response = await request(`${this.config.orgApiBaseUrl}/connect/token`, {
|
|
50
|
+
method: 'POST',
|
|
51
|
+
headers: {
|
|
52
|
+
host: `${this.config.organization}.${this.config.hostSuffix}`,
|
|
53
|
+
'content-type': 'application/x-www-form-urlencoded',
|
|
54
|
+
accept: 'application/json'
|
|
55
|
+
},
|
|
56
|
+
body: form.toString(),
|
|
57
|
+
signal: AbortSignal.timeout(this.config.requestTimeoutMs)
|
|
58
|
+
});
|
|
59
|
+
statusCode = response.statusCode;
|
|
60
|
+
const text = await response.body.text();
|
|
61
|
+
try {
|
|
62
|
+
payload = text ? JSON.parse(text) : undefined;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
payload = text;
|
|
66
|
+
}
|
|
67
|
+
break;
|
|
58
68
|
}
|
|
59
|
-
catch {
|
|
60
|
-
|
|
69
|
+
catch (error) {
|
|
70
|
+
const delay = NETWORK_RETRY_DELAYS_MS[attempt - 1];
|
|
71
|
+
if (delay === undefined || !isTransientNetworkError(error))
|
|
72
|
+
throw mapNetworkError(error, context, { attempts: attempt, readOnly: true });
|
|
73
|
+
logStep('kimlik ⟳', `POST ${this.config.orgApiBaseUrl}/connect/token · geçici ağ hatası, ${delay} ms sonra yeniden deneniyor (deneme ${attempt + 1}/${NETWORK_RETRY_DELAYS_MS.length + 1})`);
|
|
74
|
+
await this.wait(delay);
|
|
61
75
|
}
|
|
62
76
|
}
|
|
63
|
-
catch (error) {
|
|
64
|
-
throw mapNetworkError(error, context);
|
|
65
|
-
}
|
|
66
77
|
if (statusCode >= 400)
|
|
67
78
|
throw mapPlatformError(statusCode, payload, context);
|
|
68
79
|
const accessToken = payload && typeof payload === 'object' ? payload.access_token : undefined;
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"networkError": "Yanıt hiç alınamadıysa (bağlantı yok; saklanan okumalarda 8 sn süre sınırı da) ve kayıt yoksa adım hata dalına gider: Steps.<ad>.error.network true, status 503. 503'ü sunucu da döner — 'bağlantı yok' için network'e bakın (MOB-033 bilgi notu; status == 503 && !network sunucu arızasını ayıran doğru desendir, not almaz). network alanı yalnız veri önbelleği açık uygulamada yazılır. Saklanmayan isteklerde süre sınırı yoktur: yanıt vermeyen sunucuda adım bekler.",
|
|
13
13
|
"stale": "Önbellek son başarılı yanıtı aynen verir: her çağrıda değişen bir GET (sayaç, zaman, rastgele) ağ yokken eski değerini döner. Parametreler ve veri dili isteğin parçasıdır; başka parametreli çağrı başka kayıttır.",
|
|
14
14
|
"detailFromSteps": "Satırlar ve güncelleme satırı SETTER adımıyla yazılır: `{{ Steps.… }}` kod bağı ilk çizimde okunur, Steps'e yazmak düğümü yeniden okutmaz (mobile-list-detail-form ile aynı desen).",
|
|
15
|
-
"preview": "Önizleme (/dev, Studio, page_screenshot) önbellek KULLANMAZ — ayar açık olsa da her okuma ağdan gider ve cache alanı hiç yazılmaz; güncelleme satırı önizlemede gizli kalır. page_screenshot ağı
|
|
15
|
+
"preview": "Önizleme (/dev, Studio, page_screenshot) önbellek KULLANMAZ — ayar açık olsa da her okuma ağdan gider ve cache alanı hiç yazılmaz; güncelleme satırı önizlemede gizli kalır. page_screenshot network eylemiyle ağı keser (SCR-020) ama önbellek olmadığı için okuma hata yolundan döner; önbellekli davranış yalnız cihazda (uçak modu) görülür.",
|
|
16
16
|
"verify": "page_runtime_get ile sayfa paketi ve akış okunur; page_screenshot({ applicationSlug: '<uygulama>', pageSlug: 'siparisler' }) satırları çizer, txt_guncelleme görünmez."
|
|
17
17
|
},
|
|
18
18
|
"pages": [
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"visibility": "Kaydın sonucu akışa dönmez. Bekleyen ve reddedilen sayısı Device.offline.queue = { pending, rejected }, reddedilenler Device.offline.rejected (id, service, at, status, code, message). Kod adımında Page.offline.list(), Page.offline.count(), Page.offline.sync() ve Page.offline.resolve(id, 'retry' | 'discard').",
|
|
13
13
|
"sync": "Gönderim: bağlantının dönüşü, uygulamanın ön plana dönüşü ve ağla soğuk açılış. Sıra kuyruğa giriş zamanı; tek tek; 4xx kaydı reddeder ve sıra sürer, oturum (401/403), 408, 429, 5xx ve ağ hatası sırayı durdurur.",
|
|
14
14
|
"limits": "Kuyruktaki kayda bağlı ikinci işlem yok: queued yolu Steps.<ad>.data okumaz (dönen kimliği bekleyen adım çevrimdışıyken koşamaz). Dosya parametreli çağrı kuyruğa girmez — adım error'a gider, Steps.<ad>.error.code = 'fileNotQueued'. Kayıt 7 günde gönderilemezse 'expired' ile reddedilir. Kayıtlar kullanıcıya bağlıdır: başkasının bekleyeni gönderilmez, çıkışta silinmez.",
|
|
15
|
-
"preview": "Studio önizlemesi ve web çalışma zamanında kuyruk yoktur (barındırıcı depo vermez): queued dalı hiç koşmaz. page_screenshot ağı
|
|
15
|
+
"preview": "Studio önizlemesi ve web çalışma zamanında kuyruk yoktur (barındırıcı depo vermez): queued dalı hiç koşmaz. page_screenshot network eylemiyle ağı keser (SCR-020) ama yazma error dalından gider; kuyruklu davranış yalnız cihazda görülür. Açık ekran ilk çizildiği kopyayla kalır: akışa eklenen queued dalı ekran bir sonraki açılışta etkindir.",
|
|
16
16
|
"verify": "microservice_get ile offlineWritable, flow_get ile queued dalı okunur. Cihazda: bağlantıyı kes, dozu işaretle (queued dalı koşar), bağlantıyı aç; row_list'te tek kayıt."
|
|
17
17
|
},
|
|
18
18
|
"microservicePatchArgs": {
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
"list": "Liste = sunucunun yanıtı (ağsızken son okunan) + Page.offline.list()'teki status === 'pending' kayıtlar (service = yazma ucunun slug'ı, parameters'tan alan). Reddedilen kayıt (status 'rejected') listeye katılmaz. Ağ yokluğu hata metni göstermez: tx_liste_hata yalnız Steps.<okuma>.error && !error.network iken görünür.",
|
|
13
13
|
"lock": "Yazan düğme istek süresince isLoading + isDisabled. Kilitsiz düğmede hızlı ikinci dokunuş YENİ bir istek ve yeni bir Idempotency-Key'dir — sunucu ikisini de uygular (ölçüldü: 1,6 sn'de üç dokunuş, üç kayıt). Kilit her dalın sonunda (always, queued, error) açılır.",
|
|
14
14
|
"landing": "Açılış sayfası korumasız giriş ekranıdır; kalıcı state.app.oturumVar (storage: device) true ise mount akışı listeye replace ile geçer. Giriş işareti yazar, çıkış siler. Geçici çare: korumalı açılış sayfası ağsız ilk açılışta çizilemiyor (kök ADR 0024 taslak) ve kabuk bilgisi yokken middleware reddi girişe yönlenmiyor (zeyrek-frontend ARC-342). Mağaza derlemesinde açılış manifestin landingPageSlug'ıdır.",
|
|
15
|
-
"refresh": "Liste scroll-view refreshable + refresh olayı aynı yükleme akışına
|
|
16
|
-
"preview": "Studio önizlemesi, /dev ve page_screenshot önbellek ve kuyruk KULLANMAZ: page_screenshot ağı
|
|
15
|
+
"refresh": "Liste scroll-view refreshable + refresh olayı ve sayfanın offlineQueueChange olayı aynı yükleme akışına bağlı (pageUpdateArgs, kök ADR 0029): kuyruğa giren kayıt listeye hemen katılır, bağlantı dönünce gönderilen kayıt kuyruk değiştiği için akış yeniden koşup kaydı sunucudan getirir — listeden düşmez. Bağlantı dönüşünün kendisi açık ekranın mount akışını koşturmaz (kök ADR 0025 taslak); çekerek yenileme yine çalışır.",
|
|
16
|
+
"preview": "Studio önizlemesi, /dev ve page_screenshot önbellek ve kuyruk KULLANMAZ: page_screenshot network eylemiyle ağı keser (SCR-020) ama okuma da yazma da hata yolundan döner; önbellekli ve kuyruklu davranış yalnız cihazda (uçak kipi) görülür. Ekran açıkken değişen akış ekran yeniden açılınca etkindir.",
|
|
17
17
|
"notWhere": "Kuyruktaki kayda bağlı ikinci işlem (dönen kimlikle yönlendirme) çevrimdışıyken koşamaz; o akışlara queued dalı vermeyin. Giriş ve hiç açılmamış ekran ağ ister — fark yalnız orada görünür."
|
|
18
18
|
},
|
|
19
19
|
"applicationUpdateArgs": {
|
|
@@ -1514,7 +1514,16 @@
|
|
|
1514
1514
|
"event": "click",
|
|
1515
1515
|
"eventValue": "<cikis akış id>"
|
|
1516
1516
|
}
|
|
1517
|
-
]
|
|
1517
|
+
],
|
|
1518
|
+
"pageUpdateArgs": {
|
|
1519
|
+
"_note": "Sayfa olayı page_update { events } ile yazılır (mobil sayfa olayı; listeyi kuyruk değişince yeniden kurar).",
|
|
1520
|
+
"events": [
|
|
1521
|
+
{
|
|
1522
|
+
"name": "offlineQueueChange",
|
|
1523
|
+
"value": "<notlar-yukle akış id>"
|
|
1524
|
+
}
|
|
1525
|
+
]
|
|
1526
|
+
}
|
|
1518
1527
|
}
|
|
1519
1528
|
]
|
|
1520
1529
|
}
|
|
@@ -97,6 +97,15 @@ değişimidir: adresin gösterdiği **dosya kopyalanmaz**, production'da adres *
|
|
|
97
97
|
production'a taşımak için onu dosya tipli prop'a (`ref`) ya da taşınan bir satırın dosya sütununa
|
|
98
98
|
bağlayın.
|
|
99
99
|
|
|
100
|
+
## Dağıtımdan sonra doğrula
|
|
101
|
+
|
|
102
|
+
`deployment_status` bitti dedikten sonra `environment_diff({ projectId })` (varsayılan development → production)
|
|
103
|
+
iki ortamın tanım farkını tek çağrıda verir: `equal: true` ise özetlenen modüllerde fark yok. Fark tanım özetinden
|
|
104
|
+
gelir, gövdeyi elle karşılaştırmaya gerek yok. Dağıtım kimlikleri korur; ama development'ta yeniden yazılan bir akış
|
|
105
|
+
(adım parametrelerinin kimlikleri yenilenir) içeriği aynı olsa da `changed` görünebilir.
|
|
106
|
+
`notCovered`'daki modüller (tablolar, bileşenler, erişim, süreçler) ve tablo satırları bu karşılaştırmanın
|
|
107
|
+
dışındadır; satır için §Tablo satırları. `digestNote` varsa özet bayat olabilir.
|
|
108
|
+
|
|
100
109
|
## Production'a doğrudan yazılmaz
|
|
101
110
|
|
|
102
111
|
Eksik satır ya da dosya production'da elle tamamlanamaz. Production ortamında `file_upload`,
|