@appaflytech/zeyrek-ai-mcp 0.0.4 → 0.0.6
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 +8 -3
- package/.env.production.example +2 -0
- package/README.md +229 -193
- package/dist/config.js +20 -2
- package/dist/knowledge/backendConstraints.js +897 -0
- package/dist/knowledge/componentCatalog.js +196 -42
- package/dist/knowledge/coverage.js +43 -0
- package/dist/knowledge/flowGuards.js +30 -0
- package/dist/knowledge/guideSections.js +49 -0
- package/dist/knowledge/hydrate.js +97 -0
- package/dist/knowledge/languageCodes.js +36 -0
- package/dist/knowledge/languages.js +105 -0
- package/dist/knowledge/layoutSummary.js +10 -0
- package/dist/knowledge/localization.js +51 -0
- package/dist/knowledge/normalize.js +449 -16
- package/dist/knowledge/outlines.js +136 -12
- package/dist/knowledge/propTypes.js +80 -0
- package/dist/knowledge/rules.js +618 -43
- package/dist/knowledge/schemaGuard.js +199 -0
- package/dist/knowledge/styleResolve.js +63 -4
- package/dist/observability/trace.js +27 -3
- package/dist/platform/errors.js +46 -4
- package/dist/platform/http.js +33 -4
- package/dist/platform/patch.js +27 -0
- package/dist/platform/refs.js +104 -0
- package/dist/platform/responses.js +9 -2
- package/dist/platform/screenshot.js +527 -16
- package/dist/platform/secrets.js +40 -0
- package/dist/platform/tokenVault.js +89 -0
- package/dist/platform/writeDiff.js +281 -0
- package/dist/resources/examples/layout-app-shell.json +1 -1
- package/dist/resources/examples/microservice-table-list.json +2 -4
- package/dist/resources/examples/page-dynamic-form.json +2 -2
- package/dist/resources/examples/page-flex-sections.json +99 -0
- package/dist/resources/examples/page-sign-in.json +3 -2
- package/dist/resources/guides/component.md +69 -8
- package/dist/resources/guides/entity.md +404 -16
- package/dist/resources/guides/expressions.md +44 -2
- package/dist/resources/guides/external-component.md +9 -0
- package/dist/resources/guides/getting-started.md +77 -7
- package/dist/resources/guides/layout.md +53 -10
- package/dist/resources/guides/microservice.md +226 -13
- package/dist/resources/guides/middleware.md +89 -1
- package/dist/resources/guides/page.md +383 -46
- package/dist/resources/guides/translation.md +126 -8
- package/dist/resources/guides/view.md +16 -1
- package/dist/resources/guides/workflow.md +198 -27
- package/dist/resources/knowledge.js +5 -0
- package/dist/resources/rules.js +27 -0
- package/dist/resources/schema-fingerprint.json +8 -0
- package/dist/resources/schemas/CreateBulkRowCommand.json +33 -0
- package/dist/resources/schemas/CreateDatasourceCommand.json +15 -6
- package/dist/resources/schemas/CreateDiscoveredEntitiesCommand.json +19 -8
- package/dist/resources/schemas/CreateEntityCommand.json +52 -12
- package/dist/resources/schemas/CreateMicroserviceCommand.json +26 -2
- package/dist/resources/schemas/CreateQueryCommand.json +56 -3
- package/dist/resources/schemas/CreateRowCommand.json +38 -0
- package/dist/resources/schemas/CreateViewCommand.json +13 -10
- package/dist/resources/schemas/CreateWorkflowCommand.json +95 -14
- package/dist/resources/schemas/GetRowsQuery.json +3 -0
- package/dist/resources/schemas/PreviewMicroserviceCommand.json +16 -0
- package/dist/resources/schemas/PreviewQueryCommand.json +5 -2
- package/dist/resources/schemas/PreviewWorkflowCommand.json +34 -9
- package/dist/resources/schemas/StepDto.json +23 -3
- package/dist/resources/schemas/UpdateBulkRowCommand.json +33 -0
- package/dist/resources/schemas/UpdateDatasourceCommand.json +15 -6
- package/dist/resources/schemas/UpdateEntityCommand.json +49 -10
- package/dist/resources/schemas/UpdateMicroserviceCommand.json +28 -3
- package/dist/resources/schemas/UpdateQueryCommand.json +13 -3
- package/dist/resources/schemas/UpdateRowCommand.json +40 -0
- package/dist/resources/schemas/UpdateViewCommand.json +15 -10
- package/dist/resources/schemas/UpdateWorkflowCommand.json +95 -14
- package/dist/server.js +4 -0
- package/dist/tools/access.js +3 -3
- package/dist/tools/applications.js +1 -1
- package/dist/tools/automations.js +63 -30
- package/dist/tools/common.js +39 -0
- package/dist/tools/components.js +16 -7
- package/dist/tools/datasources.js +83 -21
- package/dist/tools/deployments.js +34 -2
- package/dist/tools/dtos.js +25 -12
- package/dist/tools/entities.js +113 -28
- package/dist/tools/externalComponents.js +9 -3
- package/dist/tools/files.js +20 -6
- package/dist/tools/flows.js +110 -11
- package/dist/tools/folders.js +7 -2
- package/dist/tools/globalFunctions.js +63 -0
- package/dist/tools/layouts.js +103 -29
- package/dist/tools/microservices.js +229 -38
- package/dist/tools/organizations.js +5 -6
- package/dist/tools/pages.js +365 -100
- package/dist/tools/platformInfo.js +152 -11
- package/dist/tools/projects.js +183 -33
- package/dist/tools/queries.js +32 -10
- package/dist/tools/rows.js +159 -45
- package/dist/tools/screenWrite.js +59 -0
- package/dist/tools/screens.js +28 -0
- package/dist/tools/translations.js +24 -8
- package/dist/tools/views.js +20 -10
- package/dist/tools/workflows.js +33 -16
- package/package.json +6 -3
|
@@ -1,18 +1,337 @@
|
|
|
1
1
|
import { PlatformApiError } from './errors.js';
|
|
2
|
+
import { INTERNAL_ROLE_SEPARATOR, knownInternalRoles } from '../knowledge/componentCatalog.js';
|
|
2
3
|
import { logStep } from '../observability/trace.js';
|
|
4
|
+
/**
|
|
5
|
+
* G-24 — gateway sayfayı kurabiliyor, okuyabiliyor ve çizdirebiliyordu ama üzerinde OLAY
|
|
6
|
+
* TETİKLEYEMİYORDU: "filtre değişince veri yenileniyor" iddiası yalnız şema okumasıyla
|
|
7
|
+
* kanıtlanabiliyordu. 09-05 koşusunda üç olay bağının ikisi şemada "bağlı" görünüp gerçekte
|
|
8
|
+
* çalışmıyordu (form submit hiç doğmuyordu, `code` adımının gövdesi kaydedilmemişti) ve ikisi de
|
|
9
|
+
* yalnız gerçek tıklamayla görüldü.
|
|
10
|
+
*
|
|
11
|
+
* Değer yazarken NATIVE setter kullanılır ve `input`+`change` olayları bubbles ile atılır: DOM
|
|
12
|
+
* value'sunu düz atamak framework'ün duyduğu olayı üretmez, alan dolu görünür ama akış çalışmaz.
|
|
13
|
+
* `resource` sayacı "istek gerçekten gitti mi" sorusunu cevaplar — koşuda kusuru kesinleştiren
|
|
14
|
+
* gözlem tam olarak buydu (ağ kaydında hiç istek yoktu).
|
|
15
|
+
*/
|
|
16
|
+
export function actionScript(action) {
|
|
17
|
+
const selector = JSON.stringify(`[data-element-id="${String(action.target ?? '')}"]`);
|
|
18
|
+
const value = JSON.stringify(action.value ?? '');
|
|
19
|
+
if (action.action === 'click') {
|
|
20
|
+
return `(function () {
|
|
21
|
+
var count = performance.getEntriesByType('resource').length;
|
|
22
|
+
var node = document.querySelector(${selector});
|
|
23
|
+
if (!node) return { found: false, applied: false, requests: count };
|
|
24
|
+
var target = node.matches('button, a, [role="button"]') ? node : (node.querySelector('button, a, [role="button"]') || node);
|
|
25
|
+
target.click();
|
|
26
|
+
return { found: true, applied: true, requests: count };
|
|
27
|
+
})()`;
|
|
28
|
+
}
|
|
29
|
+
return `(function () {
|
|
30
|
+
var count = performance.getEntriesByType('resource').length;
|
|
31
|
+
var node = document.querySelector(${selector});
|
|
32
|
+
if (!node) return { found: false, applied: false, requests: count };
|
|
33
|
+
var wanted = String(${value});
|
|
34
|
+
|
|
35
|
+
// Açılır kutu (dropdown / multi-select) NATIVE alan değildir: değeri yazmak
|
|
36
|
+
// hiçbir şey yapmaz, seçeneğe TIKLANMASI gerekir. Liste ancak kutu açıkken
|
|
37
|
+
// basılır, o yüzden önce kutu açılır, sonra data-value ile seçenek bulunur
|
|
38
|
+
// (bulunamazsa görünen metinle eşlenir).
|
|
39
|
+
var combobox = node.querySelector('[role="combobox"], .ng-select__value-container, .ng-multiselect__value-container');
|
|
40
|
+
if (combobox) {
|
|
41
|
+
combobox.click();
|
|
42
|
+
|
|
43
|
+
// Liste tıklamadan SONRA basılır; aynı tick'te sorgulamak boş sonuç verir.
|
|
44
|
+
// evaluate döndürülen promise'i bekler, o yüzden seçim bir sonraki tur yapılır.
|
|
45
|
+
return new Promise(function (resolve) {
|
|
46
|
+
setTimeout(function () {
|
|
47
|
+
var options = Array.prototype.slice.call(document.querySelectorAll('[role="treeitem"][data-value]'));
|
|
48
|
+
var match = options.filter(function (option) { return option.getAttribute('data-value') === wanted; })[0]
|
|
49
|
+
|| options.filter(function (option) { return (option.textContent || '').trim() === wanted; })[0];
|
|
50
|
+
if (!match) {
|
|
51
|
+
resolve({ found: true, applied: false, note: "açılır kutu açıldı ama bu değere karşılık gelen seçenek yok: " + wanted + " — options listesini page_runtime_get ile doğrulayın; seçenek sayısı: " + options.length, requests: count });
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
(match.querySelector('button') || match).click();
|
|
55
|
+
resolve({ found: true, applied: true, requests: count });
|
|
56
|
+
}, 300);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
var field = node.matches('input, select, textarea') ? node : node.querySelector('input, select, textarea');
|
|
61
|
+
if (!field) return { found: true, applied: false, note: 'düğümde input/select/textarea yok — bu komponent native alan kullanmıyor olabilir, click ile deneyin', requests: count };
|
|
62
|
+
var proto = field instanceof HTMLSelectElement ? HTMLSelectElement.prototype
|
|
63
|
+
: (field instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype);
|
|
64
|
+
if (field instanceof HTMLInputElement && (field.type === 'checkbox' || field.type === 'radio')) {
|
|
65
|
+
var shouldCheck = wanted === 'true' || wanted === '1' || wanted === 'on';
|
|
66
|
+
if (field.checked !== shouldCheck) field.click();
|
|
67
|
+
return { found: true, applied: true, requests: count };
|
|
68
|
+
}
|
|
69
|
+
var setter = Object.getOwnPropertyDescriptor(proto, 'value').set;
|
|
70
|
+
setter.call(field, ${value});
|
|
71
|
+
field.dispatchEvent(new Event('input', { bubbles: true }));
|
|
72
|
+
field.dispatchEvent(new Event('change', { bubbles: true }));
|
|
73
|
+
return { found: true, applied: true, requests: count };
|
|
74
|
+
})()`;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* fullPage karesi Chromium'un viewport ötesi yakalamasıyla değil, viewport'u içerik boyuna
|
|
78
|
+
* büyüterek alınır: Studio runtime'ı kendi kaydırma kabını `100vh` üzerine kuruyor ve viewport
|
|
79
|
+
* ötesi yakalama o kabı bir kez daha çiziyordu — aynı sayfa karede iki kez görünüyordu (09-04
|
|
80
|
+
* Y-23). Tek karede çekilen görüntüde o birleştirme adımı hiç yok.
|
|
81
|
+
*/
|
|
82
|
+
export const contentHeightScript = [
|
|
83
|
+
'(function () {',
|
|
84
|
+
' var doc = document.documentElement;',
|
|
85
|
+
' var body = document.body;',
|
|
86
|
+
' var viewportHeight = window.innerHeight;',
|
|
87
|
+
' var heights = [doc ? doc.scrollHeight : 0, body ? body.scrollHeight : 0];',
|
|
88
|
+
' var nodes = document.querySelectorAll("*");',
|
|
89
|
+
' for (var index = 0; index < nodes.length && index < 4000; index += 1) {',
|
|
90
|
+
' var node = nodes[index];',
|
|
91
|
+
' if (node.clientHeight >= viewportHeight * 0.6 && node.scrollHeight > node.clientHeight + 1) heights.push(node.scrollHeight);',
|
|
92
|
+
' }',
|
|
93
|
+
' return { contentHeight: Math.max.apply(null, heights), viewportHeight: viewportHeight };',
|
|
94
|
+
'})()'
|
|
95
|
+
].join('\n');
|
|
96
|
+
/**
|
|
97
|
+
* Kare taşmayı gösterir ama ölçmez: aynı sayfa yüklemesinden neredeyse bedavaya çıkan ikinci
|
|
98
|
+
* çıktı her runtime düğümünün efektif piksel kutusudur. Düğümler `data-element-id` taşımıyorsa
|
|
99
|
+
* (F-04 dev'e çıkana dek) liste boş döner — tool uydurmaz, layoutReportUnavailable der.
|
|
100
|
+
*/
|
|
101
|
+
export const layoutReportScript = [
|
|
102
|
+
'Array.from(document.querySelectorAll("[data-element-id]")).slice(0, 500).map(function (node) {',
|
|
103
|
+
' var rect = node.getBoundingClientRect();',
|
|
104
|
+
' return {',
|
|
105
|
+
' elementId: node.getAttribute("data-element-id"),',
|
|
106
|
+
' x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height),',
|
|
107
|
+
' overflowX: node.scrollWidth > node.clientWidth + 1,',
|
|
108
|
+
' overflowY: node.scrollHeight > node.clientHeight + 1',
|
|
109
|
+
' };',
|
|
110
|
+
'})'
|
|
111
|
+
].join('\n');
|
|
112
|
+
/**
|
|
113
|
+
* F-09 — sayfa akışı yalnız tarayıcıda koşar, bu yüzden bir setter iteratee'sinin
|
|
114
|
+
* patlamasının gateway'e ulaşan HİÇBİR yolu yoktu: akış 200 dönüyor, hedef prop
|
|
115
|
+
* yazılmıyor ve tek belirti yığında kaybolan bir bildirim oluyordu. Runtime artık
|
|
116
|
+
* yapısal hata defterini bu global anahtarda yayınlıyor (`@ng-apps/flow` →
|
|
117
|
+
* flow-runtime-errors.ts); kare çekilirken bedavaya okunur.
|
|
118
|
+
*/
|
|
119
|
+
export const flowRuntimeErrorsScript = 'Array.isArray(window.__ngAppsFlowRuntimeErrors) ? window.__ngAppsFlowRuntimeErrors.slice(0, 50) : []';
|
|
120
|
+
/**
|
|
121
|
+
* Kare ne zaman hazır? (G-30 → B-6, 2026-09-07)
|
|
122
|
+
*
|
|
123
|
+
* `delayMs` TAHMİNDİ; 09-05'te runtime koşan akış sayacını yayınlamaya başladı (`@ng-apps/flow`
|
|
124
|
+
* → flow-runtime-errors.ts) ve bekleme "running sıfır, completed > 0" olunca bitiyordu. Bu şart
|
|
125
|
+
* mount akışı OLMAYAN sayfada (giriş ekranı) hiç sağlanmaz; akış tetiklemeyen `set` eylemi de
|
|
126
|
+
* `completed`'ı artırmaz. 09-06·2 koşusunda üç eylemli giriş denemesi bu yüzden 47 sn sürdü;
|
|
127
|
+
* sentetik ölçümde sayfa 0.3 sn'de hazırken aynı çağrı 48 sn'ydi — süre sayfanın değil, bu
|
|
128
|
+
* döngünün. Hazırlık artık TEK anlık görüntüden okunur: akış sayacı + Studio'nun yükleme
|
|
129
|
+
* iskeleti (`ViewDetail.svelte` → `.view-detail > .bx--skeleton__placeholder`) + runtime
|
|
130
|
+
* düğümleri (`[data-element-id]`) + hata/bulunamadı hâli + adres. Karar `decideReadiness`'ta.
|
|
131
|
+
*
|
|
132
|
+
* Yüklenmemiş kare ASLA dönmez: bütçe dolduğunda sayfa hâlâ hazır değilse çağrı hatayla düşer
|
|
133
|
+
* (`captureUnready: true` teşhis kipi dışında). Bozuk sayfayı görmek istiyorsan hata mesajı
|
|
134
|
+
* neyin görüldüğünü zaten söyler.
|
|
135
|
+
*/
|
|
136
|
+
export const readinessScript = `(function () { /* readinessSnapshot */
|
|
137
|
+
var activity = window.__ngAppsFlowActivity;
|
|
138
|
+
return {
|
|
139
|
+
activity: activity && typeof activity.running === 'number' && typeof activity.completed === 'number' ? { running: activity.running, completed: activity.completed } : null,
|
|
140
|
+
runtimeNodes: document.querySelectorAll('[data-element-id]').length,
|
|
141
|
+
skeleton: !!document.querySelector('.view-detail > .bx--skeleton__placeholder'),
|
|
142
|
+
viewDetail: !!document.querySelector('.view-detail'),
|
|
143
|
+
errorState: !!document.querySelector('.view-detail__error, .view-detail .error-page'),
|
|
144
|
+
readyState: String(document.readyState),
|
|
145
|
+
href: String(location.href)
|
|
146
|
+
};
|
|
147
|
+
})()`;
|
|
148
|
+
/** Okunan değer gerçekten sayaç mı; değilse sayfa sayaç yayınlamıyordur (eski runtime). */
|
|
149
|
+
export function isFlowActivity(activity) {
|
|
150
|
+
if (!activity || typeof activity !== 'object')
|
|
151
|
+
return false;
|
|
152
|
+
const { running, completed } = activity;
|
|
153
|
+
return typeof running === 'number' && typeof completed === 'number';
|
|
154
|
+
}
|
|
155
|
+
/** Tarayıcıdan dönen değer anlık görüntü mü; yönlendirme sırasında evaluate düşer ve undefined kalır. */
|
|
156
|
+
export function parseReadiness(raw) {
|
|
157
|
+
if (!raw || typeof raw !== 'object')
|
|
158
|
+
return undefined;
|
|
159
|
+
const value = raw;
|
|
160
|
+
if (typeof value.runtimeNodes !== 'number' || typeof value.href !== 'string')
|
|
161
|
+
return undefined;
|
|
162
|
+
return {
|
|
163
|
+
activity: isFlowActivity(value.activity) ? { running: value.activity.running, completed: value.activity.completed } : null,
|
|
164
|
+
runtimeNodes: value.runtimeNodes,
|
|
165
|
+
skeleton: value.skeleton === true,
|
|
166
|
+
viewDetail: value.viewDetail === true,
|
|
167
|
+
errorState: value.errorState === true,
|
|
168
|
+
readyState: String(value.readyState ?? ''),
|
|
169
|
+
href: value.href
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
/** `/dev` ve `/view` aynı sayfadır (Studio `hooks.ts` reroute); yönlendirme kararı yalnız yoldan verilir. */
|
|
173
|
+
export function runtimePath(url) {
|
|
174
|
+
try {
|
|
175
|
+
return new URL(url).pathname.replace(/^\/dev\//, '/view/').replace(/\/+$/, '');
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return url;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const maxCaptureHeight = 16384;
|
|
3
182
|
const navigationTimeoutMs = 20000;
|
|
4
183
|
const apiResponseTimeoutMs = 15000;
|
|
5
184
|
const settleMs = 600;
|
|
185
|
+
/** Akış bittikten sonra Svelte'in son render'ına bırakılan pay. */
|
|
186
|
+
const flowSettleGraceMs = 250;
|
|
187
|
+
const flowPollMs = 120;
|
|
188
|
+
/** "Hiç akış koşmadı" kararı için DOM'un kaç yoklama değişmemesi gerekir (~600 ms = eski settleMs). */
|
|
189
|
+
const quietPolls = 5;
|
|
190
|
+
/** Sayaç yayınlanmayan runtime'da tek kanıt DOM durulmasıdır; daha uzun beklenir (~1 sn). */
|
|
191
|
+
const legacyQuietPolls = 8;
|
|
192
|
+
/** Runtime dışına yönlendirme: hedef sayfanın kendi JS'i çizmeyi bitirsin (~1.5 sn). */
|
|
193
|
+
const redirectQuietPolls = 12;
|
|
6
194
|
const idleCloseMs = 60000;
|
|
195
|
+
/** Etkileşim adımındaki `wait` üst sınırı; delayMs tavanıyla aynı çizgide. */
|
|
196
|
+
const maxActionWaitMs = 15000;
|
|
197
|
+
/**
|
|
198
|
+
* Navigasyon zaman aşımında toplam deneme sayısı. Soğuk Studio'da ilk `page.goto` Vite
|
|
199
|
+
* derlemesini ISITIR; ikinci deneme neredeyse her zaman geçer (09-08 S-8: aynı kare üç
|
|
200
|
+
* çağrıya ve ~3.5 dakikaya mal oldu). İkiden fazlası turu uzatır, bir şey kazandırmaz.
|
|
201
|
+
*/
|
|
202
|
+
const navigationAttempts = 2;
|
|
203
|
+
/**
|
|
204
|
+
* Yalnız zaman aşımı yeniden denenir. `net::ERR_CONNECTION_REFUSED` / DNS gibi hızlı düşen
|
|
205
|
+
* hatalarda Studio ayakta değildir; tekrar yalnız bekleme süresini iki katına çıkarır ve
|
|
206
|
+
* çağırana yanlış ipucu verir.
|
|
207
|
+
*/
|
|
208
|
+
function isNavigationTimeout(message) {
|
|
209
|
+
return /timeout/i.test(message);
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Tek anlık görüntüden "kare alınabilir" kararı; iki uçta da yanlış beklememek için tek yerde.
|
|
213
|
+
* `quiet` görüntünün kaç yoklamadır değişmediğidir, `completedBefore` bu beklemenin başındaki
|
|
214
|
+
* bitmiş akış sayısı (sayfa değiştiyse çağıran sıfırlar). İskelet görünüyorsa hiçbir karar yok.
|
|
215
|
+
*/
|
|
216
|
+
export function decideReadiness(snapshot, quiet, completedBefore, baselineUrl) {
|
|
217
|
+
if (snapshot.skeleton)
|
|
218
|
+
return undefined;
|
|
219
|
+
const { activity } = snapshot;
|
|
220
|
+
const idle = activity !== null && activity.running === 0;
|
|
221
|
+
if (idle && activity.completed > completedBefore && snapshot.runtimeNodes > 0)
|
|
222
|
+
return 'ready';
|
|
223
|
+
if (idle && snapshot.runtimeNodes > 0 && quiet >= quietPolls)
|
|
224
|
+
return 'ready-no-flows';
|
|
225
|
+
if (activity === null && snapshot.runtimeNodes > 0 && quiet >= legacyQuietPolls)
|
|
226
|
+
return 'ready-no-counter';
|
|
227
|
+
if (idle && snapshot.errorState && quiet >= quietPolls)
|
|
228
|
+
return 'error';
|
|
229
|
+
if (!snapshot.viewDetail && snapshot.readyState === 'complete' && baselineUrl !== undefined && runtimePath(snapshot.href) !== runtimePath(baselineUrl) && quiet >= redirectQuietPolls)
|
|
230
|
+
return 'redirected';
|
|
231
|
+
return undefined;
|
|
232
|
+
}
|
|
233
|
+
/** Hata mesajı için: bütçe dolduğunda sayfada NE görüldü. */
|
|
234
|
+
export function describeUnready(readiness, apiNote) {
|
|
235
|
+
const snapshot = readiness.snapshot;
|
|
236
|
+
const suffix = apiNote === undefined ? '' : ` · ${apiNote}`;
|
|
237
|
+
if (!snapshot)
|
|
238
|
+
return `tarayıcı sayfayı hiç değerlendiremedi (sürekli yönlendirme ya da bağlam düştü)${suffix}`;
|
|
239
|
+
if (snapshot.skeleton)
|
|
240
|
+
return `sayfa hâlâ yükleme iskeletinde${suffix}`;
|
|
241
|
+
if (snapshot.activity && snapshot.activity.running > 0)
|
|
242
|
+
return `${snapshot.activity.running} akış hâlâ koşuyor (${snapshot.activity.completed} bitti)${suffix}`;
|
|
243
|
+
if (snapshot.runtimeNodes === 0)
|
|
244
|
+
return `runtime hiç render olmadı — data-element-id taşıyan düğüm yok${snapshot.viewDetail ? '' : ', .view-detail kabı da yok'}${suffix}`;
|
|
245
|
+
return `sayfa durulmadı (${snapshot.runtimeNodes} düğüm, sayaç ${snapshot.activity ? `${snapshot.activity.running} koşan / ${snapshot.activity.completed} biten` : 'yok'})${suffix}`;
|
|
246
|
+
}
|
|
247
|
+
async function measureContentHeight(page) {
|
|
248
|
+
const measured = await page.evaluate(contentHeightScript).catch(() => undefined);
|
|
249
|
+
const height = measured?.contentHeight;
|
|
250
|
+
return typeof height === 'number' && Number.isFinite(height) && height > 0 ? Math.ceil(height) : undefined;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Playwright bağlamı ve tarayıcısı bu sınıfta PAYLAŞILIR. İki çekim aynı anda koştuğunda
|
|
254
|
+
* 2026-09-06 koşusunda ikisi de `Target page, context or browser has been closed` ile düştü
|
|
255
|
+
* (çağrı #204/#205, 64 sn kayıp, ajan yeniden giriş yapmak zorunda kaldı — `S-3`). Kök neden
|
|
256
|
+
* iki yerdeydi: (1) paylaşılan tarayıcı üstünde eşzamanlı bağlam açma, (2) her çekimin `finally`
|
|
257
|
+
* bloğunda ÖNCEKİ boşta-kapatma zamanlayıcısı temizlenmeden yenisinin kurulması — kaçan
|
|
258
|
+
* zamanlayıcı sonraki çekimin ortasında tarayıcıyı kapatıyordu.
|
|
259
|
+
*
|
|
260
|
+
* Çözüm bir seri kuyruk: çekimler tek tek koşar, bekleyen çağrı `queuedMs` ile ne kadar
|
|
261
|
+
* beklediğini söyler. Sıcak tarayıcı havuzu bu işin dışıdır (Ç2 · tur ekonomisi).
|
|
262
|
+
*/
|
|
7
263
|
export class StudioScreenshotter {
|
|
8
264
|
config;
|
|
9
265
|
launcher;
|
|
10
266
|
browserPromise;
|
|
11
267
|
idleTimer;
|
|
268
|
+
queue = Promise.resolve();
|
|
269
|
+
pending = 0;
|
|
270
|
+
recycleNext = false;
|
|
12
271
|
constructor(config, launcher) {
|
|
13
272
|
this.config = config;
|
|
14
273
|
this.launcher = launcher;
|
|
15
274
|
}
|
|
275
|
+
/**
|
|
276
|
+
* Kuyruk sınırsız birikmesin: bekleme üst sınırı aşılırsa çağrı tarayıcıya HİÇ girmeden
|
|
277
|
+
* anlaşılır bir hatayla döner ve sırasını öndekine zincirleyip çıkar, böylece kuyruğun geri
|
|
278
|
+
* kalanı serileşmiş kalır (Kısıt: tek yavaş çekimin arkasında kuyruk şişmemeli).
|
|
279
|
+
*/
|
|
280
|
+
async capture(request) {
|
|
281
|
+
const queuedAt = Date.now();
|
|
282
|
+
// Sayaç SENKRON artar: `Promise.all` ile aynı turda gelen iki çağrıda ikincisi beklediğini
|
|
283
|
+
// ancak böyle bilebilir (async bir bayrak o turda henüz kurulmamış olurdu).
|
|
284
|
+
const waited = this.pending > 0;
|
|
285
|
+
const previous = this.queue;
|
|
286
|
+
this.pending += 1;
|
|
287
|
+
let release;
|
|
288
|
+
this.queue = new Promise(resolve => {
|
|
289
|
+
release = resolve;
|
|
290
|
+
});
|
|
291
|
+
let timer;
|
|
292
|
+
const capExceeded = await Promise.race([
|
|
293
|
+
previous.then(() => false, () => false),
|
|
294
|
+
new Promise(resolve => {
|
|
295
|
+
timer = setTimeout(() => resolve(true), this.config.screenshotQueueWaitMs);
|
|
296
|
+
timer.unref?.();
|
|
297
|
+
})
|
|
298
|
+
]);
|
|
299
|
+
if (timer)
|
|
300
|
+
clearTimeout(timer);
|
|
301
|
+
if (capExceeded) {
|
|
302
|
+
this.pending -= 1;
|
|
303
|
+
void previous.then(release, release);
|
|
304
|
+
throw new PlatformApiError(`Ekran görüntüsü kuyruğunda ${Date.now() - queuedAt} ms beklendi ve üst sınır (${this.config.screenshotQueueWaitMs} ms) aşıldı — çekim başlatılmadı. Çekimleri seri çağırın ya da SCREENSHOT_QUEUE_WAIT_MS ile sınırı yükseltin.`, 503);
|
|
305
|
+
}
|
|
306
|
+
const queuedMs = Date.now() - queuedAt;
|
|
307
|
+
const recycled = this.recycleNext;
|
|
308
|
+
this.recycleNext = false;
|
|
309
|
+
if (waited)
|
|
310
|
+
logStep('tarayıcı ⏸', `çekim kuyrukta ${queuedMs} ms bekledi — eşzamanlı çekimler seri koşar`);
|
|
311
|
+
try {
|
|
312
|
+
const result = await this.captureOne(request);
|
|
313
|
+
return {
|
|
314
|
+
...result,
|
|
315
|
+
...(waited ? { queuedMs: Math.max(queuedMs, 1) } : {}),
|
|
316
|
+
...(recycled ? { contextRecycled: true } : {})
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
catch (error) {
|
|
320
|
+
// Bağlam/tarayıcı bozulduysa bir sonraki çağrı TEMİZ bir tarayıcıyla başlasın; hata
|
|
321
|
+
// kuyruktaki komşuya bulaşmasın.
|
|
322
|
+
if (/browser has been closed|target (page|closed)|context.*closed/i.test(error instanceof Error ? error.message : String(error))) {
|
|
323
|
+
this.recycleNext = true;
|
|
324
|
+
const expired = this.browserPromise;
|
|
325
|
+
this.browserPromise = undefined;
|
|
326
|
+
expired?.then(instance => instance.close()).catch(() => undefined);
|
|
327
|
+
}
|
|
328
|
+
throw error;
|
|
329
|
+
}
|
|
330
|
+
finally {
|
|
331
|
+
this.pending -= 1;
|
|
332
|
+
release();
|
|
333
|
+
}
|
|
334
|
+
}
|
|
16
335
|
async launch() {
|
|
17
336
|
if (this.launcher)
|
|
18
337
|
return this.launcher();
|
|
@@ -48,7 +367,124 @@ export class StudioScreenshotter {
|
|
|
48
367
|
* işaretlenir ve kare yine çekilir, çünkü bozuk/boş sayfayı görmek de bir doğrulamadır. Veri
|
|
49
368
|
* yanıtından sonra fontlar + kısa yerleşme beklemesi Svelte hydrate'ine alan bırakır.
|
|
50
369
|
*/
|
|
51
|
-
async
|
|
370
|
+
async runActions(page, request) {
|
|
371
|
+
if (!request.actions || request.actions.length === 0)
|
|
372
|
+
return undefined;
|
|
373
|
+
const outcomes = [];
|
|
374
|
+
for (const [index, action] of request.actions.entries()) {
|
|
375
|
+
if (action.action === 'wait') {
|
|
376
|
+
await page.waitForTimeout(Math.min(Math.max(Number(action.ms ?? settleMs), 0), maxActionWaitMs));
|
|
377
|
+
outcomes.push({ index, action: 'wait', found: true, applied: true, requests: 0 });
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
const snapshotBefore = parseReadiness(await page.evaluate(readinessScript).catch(() => undefined));
|
|
381
|
+
const activityBefore = snapshotBefore?.activity ?? null;
|
|
382
|
+
const raw = await page.evaluate(actionScript(action)).catch(error => ({
|
|
383
|
+
found: false,
|
|
384
|
+
applied: false,
|
|
385
|
+
note: error instanceof Error ? error.message : String(error),
|
|
386
|
+
requests: 0
|
|
387
|
+
}));
|
|
388
|
+
const before = (raw ?? {});
|
|
389
|
+
// Hedef bulunamadıysa ajan neyi yanlış yazdığını KAREDEN çıkaramaz. `__` taşıyan hedef bir
|
|
390
|
+
// iç rol denemesidir: bilinen roller sayılır. Kök hedefi bulunamadıysa sorun elementId'nin
|
|
391
|
+
// kendisindedir, şemayı okumaya yönlendirilir (P1-3 Step 3).
|
|
392
|
+
if (before.found === false && !before.note && typeof action.target === 'string') {
|
|
393
|
+
before.note = action.target.includes(INTERNAL_ROLE_SEPARATOR)
|
|
394
|
+
? `'${action.target}' sayfada yok. Bu bir İÇ ROL hedefi: biçim <elementId>__<rol> ve bilinen roller — ${knownInternalRoles().join(' · ')}. Kolon rolleri kolonun SLUG'ından türer (başlığından değil); satır sırası 0 tabanlıdır ve sayfa değişince yeniden numaralanır.`
|
|
395
|
+
: `'${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).`;
|
|
396
|
+
}
|
|
397
|
+
// Olay akışı uç çağırıyor: yerleşme beklemeden sonraki istek sayısını ölçmek "istek gitti mi"
|
|
398
|
+
// sorusunu cevaplar — koşuda kusuru kesinleştiren gözlem buydu.
|
|
399
|
+
//
|
|
400
|
+
// Bekleme ana yükleme döngüsüyle AYNI hazırlık kararıdır: akış koştuysa bitince, hiç akış
|
|
401
|
+
// tetiklenmediyse (metin alanına `set`) DOM durulunca, eylem sayfayı değiştirdiyse yeni
|
|
402
|
+
// sayfa hazır olunca çıkar. Eskiden akış tetiklemeyen her eylem `settleMs + delayMs`'in
|
|
403
|
+
// tamamını bekliyordu: 09-06·2 koşusunda üç eylemli giriş denemesi 47 sn (B-6).
|
|
404
|
+
const readiness = await this.waitForReady(page, {
|
|
405
|
+
budgetMs: apiResponseTimeoutMs + request.delayMs,
|
|
406
|
+
baselineUrl: snapshotBefore?.href ?? request.url,
|
|
407
|
+
completedBefore: activityBefore?.completed ?? Number.POSITIVE_INFINITY
|
|
408
|
+
});
|
|
409
|
+
if (readiness.state === 'not-ready' && request.captureUnready !== true) {
|
|
410
|
+
throw new PlatformApiError(`Etkileşim adımı ${index} (${action.action}${action.target === undefined ? '' : ` '${action.target}'`}) sonrasında sayfa ${readiness.waitedMs} ms içinde durulmadı, kare ALINMADI — ${describeUnready(readiness)}. Bekleme bütçesi ${apiResponseTimeoutMs + request.delayMs} ms; akış gerçekten uzun sürüyorsa delayMs ile büyütün. Yüklenmemiş hâli yine de görmek için captureUnready: true verin (kare pageState: 'not-ready' ile döner).`, 504);
|
|
411
|
+
}
|
|
412
|
+
const after = await page.evaluate("performance.getEntriesByType('resource').length").catch(() => undefined);
|
|
413
|
+
const requests = typeof after === 'number' && typeof before.requests === 'number' ? Math.max(after - before.requests, 0) : 0;
|
|
414
|
+
// Yerel bir akış (yalnız prop yazan setter) hiç uca gitmez ama işini YAPAR. Sayaç bu ayrımı
|
|
415
|
+
// veren tek kanıttır; sayaç yoksa flowRuns bilinmez kalır. Eylem sayfayı değiştirdiyse
|
|
416
|
+
// yeni sayfanın sayacı sıfırdan başlar, orada biten her akış bu eyleme yazılır.
|
|
417
|
+
const activityAfter = readiness.snapshot?.activity ?? null;
|
|
418
|
+
const navigated = snapshotBefore !== undefined && readiness.snapshot !== undefined && runtimePath(readiness.snapshot.href) !== runtimePath(snapshotBefore.href);
|
|
419
|
+
const flowRuns = activityAfter === null
|
|
420
|
+
? undefined
|
|
421
|
+
: navigated
|
|
422
|
+
? activityAfter.completed
|
|
423
|
+
: activityBefore === null ? undefined : Math.max(activityAfter.completed - activityBefore.completed, 0);
|
|
424
|
+
outcomes.push({
|
|
425
|
+
index,
|
|
426
|
+
action: String(action.action),
|
|
427
|
+
...(action.target === undefined ? {} : { target: action.target }),
|
|
428
|
+
found: before.found === true,
|
|
429
|
+
applied: before.applied === true,
|
|
430
|
+
...(before.note === undefined ? {} : { note: before.note }),
|
|
431
|
+
requests,
|
|
432
|
+
...(flowRuns === undefined ? {} : { flowRuns }),
|
|
433
|
+
pageState: readiness.state
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
logStep('tarayıcı ⤵', `${outcomes.length} etkileşim adımı koştu`, outcomes.map(outcome => `${outcome.index}. ${outcome.action}${outcome.target ? ` '${outcome.target}'` : ''} → ${outcome.found ? (outcome.applied ? 'uygulandı' : 'bulundu ama uygulanamadı') : 'hedef BULUNAMADI'} · ${outcome.requests} ağ isteği`));
|
|
437
|
+
return outcomes;
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Sayfa hazır olana kadar yoklar; hazır kararını `decideReadiness` verir. Karar verildikten
|
|
441
|
+
* sonra kısa bir pay bırakılır ve görüntü BİR KEZ DAHA doğrulanır: giriş akışının son adımı
|
|
442
|
+
* yönlendirmeyse "bitti" sinyali yeni sayfa açılırken gelir ve doğrulama olmadan kare o
|
|
443
|
+
* geçişte alınırdı. Adres değiştiyse sayaç yeni sayfada sıfırdan başlar; baz da sıfırlanır.
|
|
444
|
+
*
|
|
445
|
+
* Döngü hem süreyle hem TUR sayısıyla sınırlıdır: sahte saatle koşan testte `Date.now()`
|
|
446
|
+
* ilerlemez ve yalnız süreye bakan bir döngü hiç bitmezdi.
|
|
447
|
+
*/
|
|
448
|
+
async waitForReady(page, options) {
|
|
449
|
+
const startedAt = Date.now();
|
|
450
|
+
const deadline = startedAt + options.budgetMs;
|
|
451
|
+
const maxPolls = Math.ceil(options.budgetMs / flowPollMs) + 1;
|
|
452
|
+
let completedBefore = options.completedBefore;
|
|
453
|
+
let previousKey;
|
|
454
|
+
let quiet = 0;
|
|
455
|
+
let last;
|
|
456
|
+
for (let poll = 0; poll < maxPolls; poll++) {
|
|
457
|
+
const snapshot = parseReadiness(await page.evaluate(readinessScript).catch(() => undefined));
|
|
458
|
+
if (snapshot === undefined) {
|
|
459
|
+
// Yönlendirme sırasında evaluate düşer: sessizlik sayacı sıfırlanır, yoklama sürer.
|
|
460
|
+
previousKey = undefined;
|
|
461
|
+
quiet = 0;
|
|
462
|
+
}
|
|
463
|
+
else {
|
|
464
|
+
last = snapshot;
|
|
465
|
+
if (runtimePath(snapshot.href) !== runtimePath(options.baselineUrl))
|
|
466
|
+
completedBefore = 0;
|
|
467
|
+
const key = JSON.stringify(snapshot);
|
|
468
|
+
quiet = key === previousKey ? quiet + 1 : 0;
|
|
469
|
+
previousKey = key;
|
|
470
|
+
const state = decideReadiness(snapshot, quiet, completedBefore, options.baselineUrl);
|
|
471
|
+
if (state !== undefined) {
|
|
472
|
+
await page.waitForTimeout(flowSettleGraceMs);
|
|
473
|
+
const confirmed = parseReadiness(await page.evaluate(readinessScript).catch(() => undefined));
|
|
474
|
+
if (confirmed !== undefined && !confirmed.skeleton && confirmed.href === snapshot.href && (confirmed.activity === null || confirmed.activity.running === 0)) {
|
|
475
|
+
return { state, waitedMs: Date.now() - startedAt, snapshot: confirmed };
|
|
476
|
+
}
|
|
477
|
+
previousKey = undefined;
|
|
478
|
+
quiet = 0;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
if (Date.now() >= deadline || poll === maxPolls - 1)
|
|
482
|
+
break;
|
|
483
|
+
await page.waitForTimeout(flowPollMs);
|
|
484
|
+
}
|
|
485
|
+
return { state: 'not-ready', waitedMs: Date.now() - startedAt, ...(last === undefined ? {} : { snapshot: last }) };
|
|
486
|
+
}
|
|
487
|
+
async captureOne(request) {
|
|
52
488
|
const startedAt = Date.now();
|
|
53
489
|
if (this.idleTimer)
|
|
54
490
|
clearTimeout(this.idleTimer);
|
|
@@ -69,32 +505,107 @@ export class StudioScreenshotter {
|
|
|
69
505
|
});
|
|
70
506
|
context = await browser.newContext({ viewport: request.viewport, deviceScaleFactor: 1 });
|
|
71
507
|
}
|
|
72
|
-
logStep('tarayıcı →', `GET ${request.url} · viewport ${request.viewport.width}x${request.viewport.height}${request.fullPage ? ' · fullPage' : ''}${request.token ? '' : ' · ANONİM (çerez YOK)'}`);
|
|
508
|
+
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)'}`);
|
|
73
509
|
try {
|
|
74
|
-
|
|
75
|
-
|
|
510
|
+
const cookies = new Map();
|
|
511
|
+
if (request.token)
|
|
512
|
+
cookies.set('NG_AccessToken', request.token);
|
|
513
|
+
for (const cookie of request.cookies ?? [])
|
|
514
|
+
cookies.set(cookie.name, cookie.value);
|
|
515
|
+
if (cookies.size > 0) {
|
|
516
|
+
await context.addCookies([...cookies].map(([name, value]) => ({ name, value, url: request.cookieUrl, httpOnly: false })));
|
|
76
517
|
}
|
|
77
518
|
const page = await context.newPage();
|
|
78
|
-
|
|
79
|
-
|
|
519
|
+
// Soğuk Vite derlemesinde sayfanın kendi veri isteği 15 sn'yi aşabiliyor; delayMs bu
|
|
520
|
+
// bütçeyi de uzatır (09-02 E-2). Yanıt hazırlık döngüsünü BLOKLAMAZ: runtime dışına
|
|
521
|
+
// yönlendirilen sayfa bu isteği hiç atmaz ve eskiden 15 sn boşa bekleniyordu.
|
|
522
|
+
const readyBudgetMs = apiResponseTimeoutMs + request.delayMs;
|
|
523
|
+
const watchApiResponse = () => page
|
|
524
|
+
.waitForResponse(response => response.url().includes(request.apiPathMarker), { timeout: readyBudgetMs })
|
|
80
525
|
.then(response => response.status(), () => undefined);
|
|
81
|
-
|
|
82
|
-
|
|
526
|
+
// `delayMs` navigasyon bütçesini de uzatır. Eskiden sabit 20 sn'ydi ve soğuk Studio'da
|
|
527
|
+
// ajanın elinde tek çıkış çağrıyı tekrarlamaktı (09-08 S-8) — "sabrım bu kadar" demenin
|
|
528
|
+
// tek anlamı olsun diye iki bütçe de aynı düğmeye bağlı.
|
|
529
|
+
const navigationTimeout = navigationTimeoutMs + request.delayMs;
|
|
530
|
+
let observedApiStatus = watchApiResponse();
|
|
531
|
+
let navigationRetried = false;
|
|
532
|
+
for (let attempt = 1;; attempt++) {
|
|
533
|
+
try {
|
|
534
|
+
await page.goto(request.url, { waitUntil: 'domcontentloaded', timeout: navigationTimeout });
|
|
535
|
+
break;
|
|
536
|
+
}
|
|
537
|
+
catch (error) {
|
|
538
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
539
|
+
const timedOut = isNavigationTimeout(message);
|
|
540
|
+
if (!timedOut || attempt >= navigationAttempts) {
|
|
541
|
+
const attemptNote = attempt > 1 ? ` (${attempt} deneme)` : '';
|
|
542
|
+
const budgetNote = timedOut
|
|
543
|
+
? ` Navigasyon bütçesi ${navigationTimeout} ms'ydi; soğuk Studio'da delayMs'i büyütün — o değer artık navigasyonu da uzatır.`
|
|
544
|
+
: '';
|
|
545
|
+
throw new PlatformApiError(`Studio'ya ulaşılamadı${attemptNote}: ${request.url} — ${message}.${budgetNote} STUDIO_BASE_URL doğru mu, arayüz ayakta mı? (yerelde zeyrek-frontend/apps/studio içinde \`pnpm dev\`)`);
|
|
546
|
+
}
|
|
547
|
+
navigationRetried = true;
|
|
548
|
+
// Veri isteği gözlemi de sıfırlanır: ilk denemeyi bekleyen sayaç çoktan dolmuş olur ve
|
|
549
|
+
// yeniden denenen navigasyonun yanıtını kaçırıp boş loader karesi damgalardı.
|
|
550
|
+
observedApiStatus = watchApiResponse();
|
|
551
|
+
logStep('tarayıcı ⟳', `navigasyon ${navigationTimeout} ms'de düştü, bir kez daha deneniyor — ilk deneme soğuk derlemeyi ısıttı`);
|
|
552
|
+
}
|
|
83
553
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
554
|
+
const readiness = await this.waitForReady(page, { budgetMs: readyBudgetMs, baselineUrl: request.url, completedBefore: 0 });
|
|
555
|
+
// Gözlendiyse dolu, hâlâ bekleniyorsa (yönlendirme, hata) undefined — ikinci vaat hemen çözülür.
|
|
556
|
+
const apiStatus = await Promise.race([observedApiStatus, Promise.resolve(undefined)]);
|
|
557
|
+
const apiNote = apiStatus === undefined ? 'sayfanın veri isteği gözlenmedi' : `veri isteği HTTP ${apiStatus}`;
|
|
558
|
+
if (readiness.state === 'not-ready' && request.captureUnready !== true) {
|
|
559
|
+
throw new PlatformApiError(`Sayfa ${readiness.waitedMs} ms içinde yüklenmedi, kare ALINMADI — ${describeUnready(readiness, apiNote)}. Bekleme bütçesi ${readyBudgetMs} ms; sayfa yalnız yavaşsa delayMs ile büyütün. Sayfa gerçekten bozuksa page_runtime_get ile şemayı, STUDIO_BASE_URL ve slug'ları doğrulayın. Yüklenmemiş hâli yine de görmek için captureUnready: true verin (kare pageState: 'not-ready' ile döner).`, 504);
|
|
87
560
|
}
|
|
88
|
-
const apiStatus = await observedApiStatus;
|
|
89
561
|
await page.evaluate('document.fonts.ready.then(() => true)').catch(() => undefined);
|
|
90
|
-
|
|
91
|
-
|
|
562
|
+
const flowsSettled = readiness.state === 'ready' || readiness.state === 'ready-no-flows'
|
|
563
|
+
? true
|
|
564
|
+
: readiness.state === 'not-ready' ? false : undefined;
|
|
565
|
+
const finalHref = readiness.snapshot?.href;
|
|
566
|
+
const finalUrl = finalHref !== undefined && runtimePath(finalHref) !== runtimePath(request.url) ? finalHref : undefined;
|
|
567
|
+
const measured = request.fullPage ? await measureContentHeight(page) : undefined;
|
|
568
|
+
const captureHeight = measured === undefined ? undefined : Math.min(measured, maxCaptureHeight);
|
|
569
|
+
if (captureHeight !== undefined && captureHeight > request.viewport.height) {
|
|
570
|
+
await page.setViewportSize({ width: request.viewport.width, height: captureHeight });
|
|
571
|
+
await page.waitForTimeout(settleMs);
|
|
572
|
+
}
|
|
573
|
+
const actions = await this.runActions(page, request);
|
|
574
|
+
const layout = request.layoutReport === true
|
|
575
|
+
? await page.evaluate(layoutReportScript).then(value => Array.isArray(value) ? value : []).catch(() => [])
|
|
576
|
+
: undefined;
|
|
577
|
+
const runtimeErrors = await page.evaluate(flowRuntimeErrorsScript)
|
|
578
|
+
.then(value => Array.isArray(value) ? value : [])
|
|
579
|
+
.catch(() => []);
|
|
580
|
+
const png = await page.screenshot({ type: 'png', fullPage: false });
|
|
92
581
|
const elapsedMs = Date.now() - startedAt;
|
|
93
|
-
logStep('tarayıcı ←', `PNG ${png.byteLength} B · ${elapsedMs} ms · sayfa API yanıtı ${apiStatus === undefined ? 'gözlenmedi' : `HTTP ${apiStatus}`}`);
|
|
94
|
-
|
|
582
|
+
logStep('tarayıcı ←', `PNG ${png.byteLength} B · ${elapsedMs} ms · hazırlık ${readiness.state} (${readiness.waitedMs} ms) · sayfa API yanıtı ${apiStatus === undefined ? 'gözlenmedi' : `HTTP ${apiStatus}`}${finalUrl === undefined ? '' : ` · yönlendi → ${finalUrl}`}`);
|
|
583
|
+
if (runtimeErrors.length > 0) {
|
|
584
|
+
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 ?? ''}`));
|
|
585
|
+
}
|
|
586
|
+
return {
|
|
587
|
+
png,
|
|
588
|
+
apiStatus,
|
|
589
|
+
renderWaitTimedOut: apiStatus === undefined,
|
|
590
|
+
elapsedMs,
|
|
591
|
+
pageState: readiness.state,
|
|
592
|
+
readyAfterMs: readiness.waitedMs,
|
|
593
|
+
...(finalUrl === undefined ? {} : { finalUrl }),
|
|
594
|
+
...(measured === undefined ? {} : { contentHeight: measured }),
|
|
595
|
+
...(measured !== undefined && measured > maxCaptureHeight ? { fullPageTruncated: true } : {}),
|
|
596
|
+
...(layout === undefined ? {} : { layout }),
|
|
597
|
+
...(actions === undefined ? {} : { actions }),
|
|
598
|
+
...(runtimeErrors.length > 0 ? { runtimeErrors } : {}),
|
|
599
|
+
...(flowsSettled === undefined ? {} : { flowsSettled }),
|
|
600
|
+
...(navigationRetried ? { navigationRetried: true } : {})
|
|
601
|
+
};
|
|
95
602
|
}
|
|
96
603
|
finally {
|
|
97
604
|
await context.close().catch(() => undefined);
|
|
605
|
+
// Zamanlayıcı ÖNCE temizlenir: eskiden her çekim yenisini kurup eskisini bırakıyordu ve
|
|
606
|
+
// kaçan zamanlayıcı sonraki çekimin ortasında tarayıcıyı kapatıyordu (S-3).
|
|
607
|
+
if (this.idleTimer)
|
|
608
|
+
clearTimeout(this.idleTimer);
|
|
98
609
|
this.idleTimer = setTimeout(() => {
|
|
99
610
|
const expired = this.browserPromise;
|
|
100
611
|
this.browserPromise = undefined;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Desen `trace.ts`'in eski kopyasından DARALTILDI: `pass` parçası `passed`/`bypass` gibi
|
|
3
|
+
* anahtarları da yakalıyordu. Günlükte zararsızdı, tool yanıtında veri kaybı olurdu.
|
|
4
|
+
*/
|
|
5
|
+
export const secretKeyPattern = /password|passwd|secret|token|authorization|api[-_]?key|credential|connection[-_]?string/i;
|
|
6
|
+
import { looksLikeJwt, rememberToken } from './tokenVault.js';
|
|
7
|
+
const maskedValue = '***';
|
|
8
|
+
const maxPaths = 20;
|
|
9
|
+
const maxDepth = 12;
|
|
10
|
+
function walk(value, path, masked, refs, depth) {
|
|
11
|
+
if (depth > maxDepth)
|
|
12
|
+
return value;
|
|
13
|
+
if (Array.isArray(value))
|
|
14
|
+
return value.map((entry, index) => walk(entry, `${path}[${index}]`, masked, refs, depth + 1));
|
|
15
|
+
if (value && typeof value === 'object') {
|
|
16
|
+
const result = {};
|
|
17
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
18
|
+
const child = path === '' ? key : `${path}.${key}`;
|
|
19
|
+
if (secretKeyPattern.test(key) && entry !== null && entry !== undefined && entry !== '') {
|
|
20
|
+
if (masked.length < maxPaths)
|
|
21
|
+
masked.push(child);
|
|
22
|
+
// Maskelenen değer bir JWT ise kimlik testinin yolu kapanmasın: jeton kasada tutulur ve
|
|
23
|
+
// yanıta yalnız referansı döner (page_screenshot/page_runtime_get asUser onu çözer).
|
|
24
|
+
// Parola ve API anahtarı bu şekle uymadığı için referans almaz.
|
|
25
|
+
if (looksLikeJwt(entry) && Object.keys(refs).length < maxPaths)
|
|
26
|
+
refs[child] = rememberToken(entry);
|
|
27
|
+
result[key] = maskedValue;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
result[key] = walk(entry, child, masked, refs, depth + 1);
|
|
31
|
+
}
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
export function maskSecrets(payload) {
|
|
37
|
+
const masked = [];
|
|
38
|
+
const tokenRefs = {};
|
|
39
|
+
return { payload: walk(payload, '', masked, tokenRefs, 0), masked, tokenRefs };
|
|
40
|
+
}
|