@appaflytech/zeyrek-ai-mcp 0.0.33 → 0.0.35

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.
Files changed (170) hide show
  1. package/.env.example +0 -15
  2. package/dist/config.js +0 -11
  3. package/dist/index.js +0 -2
  4. package/dist/knowledge/boxSections.js +184 -0
  5. package/dist/knowledge/componentCatalog.js +634 -255
  6. package/dist/knowledge/componentEvents.js +38 -32
  7. package/dist/knowledge/layoutSummary.js +2 -140
  8. package/dist/knowledge/normalize.js +144 -117
  9. package/dist/knowledge/rules.js +42 -291
  10. package/dist/knowledge/styleResolve.js +53 -58
  11. package/dist/knowledge/themeResolve.js +4 -4
  12. package/dist/knowledge/themeSchema.js +2 -1
  13. package/dist/observability/trace.js +1 -1
  14. package/dist/platform/diagnostics.js +4 -10
  15. package/dist/platform/domSummary.js +1 -1
  16. package/dist/platform/http.js +7 -23
  17. package/dist/platform/responses.js +2 -6
  18. package/dist/platform/screenshot.js +1162 -0
  19. package/dist/platform/secrets.js +0 -9
  20. package/dist/platform/strictArguments.js +12 -68
  21. package/dist/platform/tokenVault.js +4 -187
  22. package/dist/resources/examples/layout-app-shell.json +707 -229
  23. package/dist/resources/examples/page-flex-sections.json +1 -1
  24. package/dist/resources/guides/component.md +6 -14
  25. package/dist/resources/guides/entity.md +5 -3
  26. package/dist/resources/guides/expressions.md +5 -14
  27. package/dist/resources/guides/functions.md +1 -1
  28. package/dist/resources/guides/layout.md +58 -55
  29. package/dist/resources/guides/microservice.md +2 -9
  30. package/dist/resources/guides/middleware.md +2 -34
  31. package/dist/resources/guides/page.md +179 -256
  32. package/dist/resources/guides/theme.md +3 -8
  33. package/dist/resources/guides/translation.md +1 -1
  34. package/dist/resources/guides/workflow.md +1 -7
  35. package/dist/resources/schema-fingerprint.json +4 -4
  36. package/dist/resources/schemas/GlobalFunctionUpdateResultDto.json +1 -3
  37. package/dist/resources/schemas/MicroserviceDto.json +2 -2
  38. package/dist/server.js +1 -7
  39. package/dist/tools/applications.js +3 -60
  40. package/dist/tools/components.js +16 -103
  41. package/dist/tools/datasources.js +1 -1
  42. package/dist/tools/designSchemaPatch.js +19 -23
  43. package/dist/tools/externalComponents.js +4 -25
  44. package/dist/tools/flows.js +13 -80
  45. package/dist/tools/layouts.js +20 -48
  46. package/dist/tools/microservices.js +10 -32
  47. package/dist/tools/pages.js +115 -396
  48. package/dist/tools/platformInfo.js +2 -7
  49. package/dist/tools/rows.js +1 -7
  50. package/dist/tools/schemaOps.js +4 -35
  51. package/dist/tools/screenWrite.js +37 -25
  52. package/dist/tools/screens.js +4 -91
  53. package/dist/tools/variables.js +5 -11
  54. package/package.json +2 -2
  55. package/dist/knowledge/access.js +0 -802
  56. package/dist/knowledge/case.js +0 -41
  57. package/dist/knowledge/connectDataBind.js +0 -819
  58. package/dist/knowledge/connectDataDefinition.js +0 -341
  59. package/dist/knowledge/connectDataExecute.js +0 -93
  60. package/dist/knowledge/connectDataRules.js +0 -128
  61. package/dist/knowledge/decision.js +0 -121
  62. package/dist/knowledge/expressionPrelude.js +0 -59
  63. package/dist/knowledge/microserviceParameters.js +0 -58
  64. package/dist/knowledge/mining.js +0 -94
  65. package/dist/knowledge/mobileComponentCatalog.js +0 -864
  66. package/dist/knowledge/mobileComponentEvents.js +0 -239
  67. package/dist/knowledge/mobileStore.js +0 -227
  68. package/dist/knowledge/platform/lint.js +0 -385
  69. package/dist/knowledge/platform/mobile.js +0 -142
  70. package/dist/knowledge/platform/mobileLayoutLint.js +0 -324
  71. package/dist/knowledge/platform/mobileRules.js +0 -231
  72. package/dist/knowledge/platform/mobileSpacing.js +0 -82
  73. package/dist/knowledge/platform/mobileTabs.js +0 -108
  74. package/dist/knowledge/platform/nativeBoxModel.js +0 -44
  75. package/dist/knowledge/platform/profile.js +0 -25
  76. package/dist/knowledge/platform/resolve.js +0 -122
  77. package/dist/knowledge/platform/web.js +0 -39
  78. package/dist/knowledge/process.js +0 -510
  79. package/dist/knowledge/rowFilter.js +0 -43
  80. package/dist/knowledge/styleResolveNative.js +0 -527
  81. package/dist/platform/argumentAliases.js +0 -128
  82. package/dist/platform/bodyFile.js +0 -103
  83. package/dist/platform/capture/actions.js +0 -333
  84. package/dist/platform/capture/browser.js +0 -1
  85. package/dist/platform/capture/capture.js +0 -659
  86. package/dist/platform/capture/console-log.js +0 -122
  87. package/dist/platform/capture/index.js +0 -11
  88. package/dist/platform/capture/readiness.js +0 -103
  89. package/dist/platform/capture/summary.js +0 -77
  90. package/dist/platform/capture/target.js +0 -5
  91. package/dist/platform/capture/targets/studio-mobile.js +0 -15
  92. package/dist/platform/capture/targets/studio-web.js +0 -5
  93. package/dist/platform/impersonation.js +0 -45
  94. package/dist/platform/jsonArray.js +0 -24
  95. package/dist/platform/schemaReadMemory.js +0 -60
  96. package/dist/resources/examples/bpm-gorev-kontrol.json +0 -294
  97. package/dist/resources/examples/bpm-iso-dokuman.json +0 -228
  98. package/dist/resources/examples/bpm-satinalma.json +0 -376
  99. package/dist/resources/examples/connect-data-convert-verify.json +0 -107
  100. package/dist/resources/examples/connect-data-dropdown-roles.json +0 -66
  101. package/dist/resources/examples/connect-data-edit-m2m.json +0 -73
  102. package/dist/resources/examples/connect-data-query-params.json +0 -50
  103. package/dist/resources/examples/connect-data-table-entity.json +0 -147
  104. package/dist/resources/examples/connect-data-workflow-microservice.json +0 -65
  105. package/dist/resources/examples/mobile-app-shell-drawer.json +0 -244
  106. package/dist/resources/examples/mobile-app-shell-stack.json +0 -157
  107. package/dist/resources/examples/mobile-app-shell-tab.json +0 -208
  108. package/dist/resources/examples/mobile-form-screen.json +0 -365
  109. package/dist/resources/examples/mobile-list-detail-form.json +0 -703
  110. package/dist/resources/guides/access.md +0 -796
  111. package/dist/resources/guides/bpm.md +0 -686
  112. package/dist/resources/guides/case.md +0 -153
  113. package/dist/resources/guides/decision.md +0 -185
  114. package/dist/resources/guides/mobile-layout.md +0 -193
  115. package/dist/resources/guides/mobile.md +0 -578
  116. package/dist/resources/read.js +0 -41
  117. package/dist/resources/schemas/AccessExplanationDto.json +0 -144
  118. package/dist/resources/schemas/AccessMatrixDto.json +0 -170
  119. package/dist/resources/schemas/AccessPermissionDto.json +0 -57
  120. package/dist/resources/schemas/AccessPolicyDto.json +0 -223
  121. package/dist/resources/schemas/AccessResourceDto.json +0 -86
  122. package/dist/resources/schemas/AppSessionDto.json +0 -53
  123. package/dist/resources/schemas/ConnectDataDescribeDto.json +0 -344
  124. package/dist/resources/schemas/ConnectDataRequestModel.json +0 -202
  125. package/dist/resources/schemas/ConnectDataResultDto.json +0 -421
  126. package/dist/resources/schemas/ConnectorCredentialDto.json +0 -70
  127. package/dist/resources/schemas/ConvertConnectDataToMicroserviceCommand.json +0 -37
  128. package/dist/resources/schemas/CreateAccessPolicyCommand.json +0 -209
  129. package/dist/resources/schemas/CreateDecisionCommand.json +0 -221
  130. package/dist/resources/schemas/CreateProcessDefinitionCommand.json +0 -1337
  131. package/dist/resources/schemas/DecisionDto.json +0 -302
  132. package/dist/resources/schemas/DetachConnectDataMicroserviceCommand.json +0 -28
  133. package/dist/resources/schemas/ExplainAccessQuery.json +0 -33
  134. package/dist/resources/schemas/GenerateProcessDocumentCommand.json +0 -116
  135. package/dist/resources/schemas/OrganizationProjectDto.json +0 -335
  136. package/dist/resources/schemas/OrganizationSchemaDto.json +0 -196
  137. package/dist/resources/schemas/OrganizationSchemaVersionDto.json +0 -207
  138. package/dist/resources/schemas/PatchProcessDefinitionCommand.json +0 -76
  139. package/dist/resources/schemas/ProcessDefinitionDto.json +0 -1480
  140. package/dist/resources/schemas/ProcessSimulationDto.json +0 -96
  141. package/dist/resources/schemas/SaveConnectorCredentialCommand.json +0 -90
  142. package/dist/resources/schemas/SaveProcessEventSourceCommand.json +0 -61
  143. package/dist/resources/schemas/SaveProcessMailSourceCommand.json +0 -89
  144. package/dist/resources/schemas/SignProcessTaskCommand.json +0 -73
  145. package/dist/resources/schemas/SimulateProcessDefinitionQuery.json +0 -92
  146. package/dist/resources/schemas/StartMobileBuildCommand.json +0 -54
  147. package/dist/resources/schemas/UpdateAccessPolicyCommand.json +0 -209
  148. package/dist/resources/schemas/UpdateDecisionCommand.json +0 -216
  149. package/dist/resources/schemas/UpdateProcessDefinitionCommand.json +0 -1357
  150. package/dist/resources/schemas/UpsertMobileStoreSettingsCommand.json +0 -259
  151. package/dist/resources/schemas/ValidateProcessDefinitionQuery.json +0 -1339
  152. package/dist/tools/accessOperations.js +0 -135
  153. package/dist/tools/accessPolicies.js +0 -170
  154. package/dist/tools/accessRuntime.js +0 -203
  155. package/dist/tools/bpm.js +0 -343
  156. package/dist/tools/bpmGovernance.js +0 -266
  157. package/dist/tools/bpmOperations.js +0 -230
  158. package/dist/tools/bpmReporting.js +0 -154
  159. package/dist/tools/bpmRouting.js +0 -150
  160. package/dist/tools/bpmRuntime.js +0 -396
  161. package/dist/tools/case.js +0 -178
  162. package/dist/tools/caseRuntime.js +0 -89
  163. package/dist/tools/connectData.js +0 -449
  164. package/dist/tools/connectors.js +0 -193
  165. package/dist/tools/decisions.js +0 -206
  166. package/dist/tools/mobile.js +0 -221
  167. package/dist/tools/mobileStore.js +0 -268
  168. package/dist/tools/modules.js +0 -48
  169. package/dist/tools/organizationStructure.js +0 -137
  170. package/dist/tools/profileWrite.js +0 -92
@@ -1,659 +0,0 @@
1
- import { PlatformApiError } from '../errors.js';
2
- import { parseDomSummary } from '../domSummary.js';
3
- import { INTERNAL_ROLE_SEPARATOR, knownInternalRoles } from '../../knowledge/componentCatalog.js';
4
- import { logStep } from '../../observability/trace.js';
5
- import { actionScript, activeTabScript, rootElementId, runtimePropsScript } from './actions.js';
6
- import { domSummaryScript } from './summary.js';
7
- import { createPageConsoleLog, describePageFailure, isExpressionEcho, parseRenderErrors, renderErrorsScript, splitExpressionErrors } from './console-log.js';
8
- import { decideReadiness, describeUnready, parseReadiness, readinessScript, runtimePath } from './readiness.js';
9
- /**
10
- * Ziyaretçinin tema tercihinin yaşadığı localStorage anahtarı — `theme-toggle` bunu yazar,
11
- * `Theme` okur ve projenin kendi modunun önüne geçirir
12
- * (`zeyrek-frontend/packages/ng-apps-ui/src/shared/theme/stores.ts`). Anahtar uygulama başına
13
- * DEĞİL tarayıcı başına tektir.
14
- */
15
- const VIEWER_MODE_STORAGE_KEY = 'zeyrek-viewer-theme-mode';
16
- /**
17
- * fullPage karesi Chromium'un viewport ötesi yakalamasıyla değil, viewport'u içerik boyuna
18
- * büyüterek alınır: Studio runtime'ı kendi kaydırma kabını `100vh` üzerine kuruyor ve viewport
19
- * ötesi yakalama o kabı bir kez daha çiziyordu — aynı sayfa karede iki kez görünüyordu (09-04
20
- * Y-23). Tek karede çekilen görüntüde o birleştirme adımı hiç yok.
21
- *
22
- * İç kaydırıcı için gereken viewport `innerHeight + (scrollHeight − clientHeight)`'tir, kabın
23
- * `scrollHeight`'i DEĞİL: kap viewport'la birlikte büyür ama üstündeki/altındaki sabit parçalar
24
- * (mobil host'ta app bar + tab bar, ZM-23 f-06) büyümez. Yalnız `scrollHeight` itilince viewport
25
- * içerik boyuna çıkıyor, kap hâlâ o parçalar kadar kısa kalıyor ve uzun mobil sayfanın altı
26
- * karenin dışında kalıyordu. Kap viewport kadar yüksekse iki formül aynı sonucu verir.
27
- */
28
- export const contentHeightScript = [
29
- '(function () {',
30
- ' var doc = document.documentElement;',
31
- ' var body = document.body;',
32
- ' var viewportHeight = window.innerHeight;',
33
- ' var heights = [doc ? doc.scrollHeight : 0, body ? body.scrollHeight : 0];',
34
- ' var nodes = document.querySelectorAll("*");',
35
- ' for (var index = 0; index < nodes.length && index < 4000; index += 1) {',
36
- ' var node = nodes[index];',
37
- ' if (node.clientHeight >= viewportHeight * 0.6 && node.scrollHeight > node.clientHeight + 1) heights.push(viewportHeight + node.scrollHeight - node.clientHeight);',
38
- ' }',
39
- ' return { contentHeight: Math.max.apply(null, heights), viewportHeight: viewportHeight };',
40
- '})()'
41
- ].join('\n');
42
- /**
43
- * Kırpma kutusu CSS pikselinde okunur; kare çekilirken Chromium onu `scale` ile çarpar
44
- * (ölçüldü 2026-09-16: viewport 1280x800 · scale 0.5 · clip 300x100 → 150x50 raster).
45
- * `null` düğüm yok demektir — tool bunu HATAYA çevirir, boş kare döndürmez.
46
- */
47
- export function clipRectScript(elementId) {
48
- return `(function () { /* clipRect */
49
- var node = document.querySelector(${JSON.stringify(`[data-element-id="${elementId}"]`)});
50
- if (!node) return null;
51
- var rect = node.getBoundingClientRect();
52
- var x = rect.x + (window.scrollX || 0);
53
- var y = rect.y + (window.scrollY || 0);
54
- // Negatif kenar kırpılırken BOY da kısalır; yoksa kutu aşağı kayar ve kare düğümün altını gösterir.
55
- return { x: Math.max(x, 0), y: Math.max(y, 0), width: rect.width + Math.min(x, 0), height: rect.height + Math.min(y, 0) };
56
- })()`;
57
- }
58
- /**
59
- * Karenin raster boyu başlıktan okunur: PNG'de IHDR, JPEG'de ilk SOF işaretçisi. Ölçeğin
60
- * gerçekten uygulandığı yanıtta SAYIYLA görünsün diye var — "yarı çözünürlük istedim" ile
61
- * "yarı çözünürlük aldım" arasındaki farkı başka hiçbir alan söylemiyor.
62
- */
63
- export function imageSize(buffer) {
64
- if (buffer.length > 24 && buffer.subarray(1, 4).toString() === 'PNG') {
65
- return { width: buffer.readUInt32BE(16), height: buffer.readUInt32BE(20) };
66
- }
67
- if (buffer.length > 4 && buffer[0] === 0xff && buffer[1] === 0xd8) {
68
- let offset = 2;
69
- while (offset + 9 < buffer.length) {
70
- if (buffer[offset] !== 0xff) {
71
- offset += 1;
72
- continue;
73
- }
74
- const marker = buffer[offset + 1] ?? 0;
75
- const length = buffer.readUInt16BE(offset + 2);
76
- // SOF0-SOF3 · SOF5-SOF7 · SOF9-SOF11 · SOF13-SOF15 boyutu taşır; DHT/DRI gibi işaretçiler atlanır.
77
- if (marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) {
78
- return { height: buffer.readUInt16BE(offset + 5), width: buffer.readUInt16BE(offset + 7) };
79
- }
80
- offset += 2 + length;
81
- }
82
- }
83
- return undefined;
84
- }
85
- /**
86
- * Kare taşmayı gösterir ama ölçmez: aynı sayfa yüklemesinden neredeyse bedavaya çıkan ikinci
87
- * çıktı her runtime düğümünün efektif piksel kutusudur. Düğümler `data-element-id` taşımıyorsa
88
- * (F-04 dev'e çıkana dek) liste boş döner — tool uydurmaz, layoutReportUnavailable der.
89
- */
90
- export const layoutReportScript = [
91
- '(function () {',
92
- 'var nodes = Array.from(document.querySelectorAll("[data-element-id]")).slice(0, 500);',
93
- 'return nodes.map(function (node) {',
94
- ' var rect = node.getBoundingClientRect();',
95
- ' var parent = node.parentElement ? node.parentElement.closest("[data-element-id]") : null;',
96
- ' var parentIndex = parent ? nodes.indexOf(parent) : -1;',
97
- ' var style = window.getComputedStyle(node);',
98
- ' var painted = function (color) { return color !== "transparent" && !/rgba\\([^)]*,\\s*0\\)$/.test(color); };',
99
- ' var bordered = ["Top", "Right", "Bottom", "Left"].some(function (side) {',
100
- ' return parseFloat(style["border" + side + "Width"]) > 0 && style["border" + side + "Style"] !== "none" && painted(style["border" + side + "Color"]);',
101
- ' });',
102
- ' return {',
103
- ' elementId: node.getAttribute("data-element-id"),',
104
- ' x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height),',
105
- ' overflowX: node.scrollWidth > node.clientWidth + 1,',
106
- ' overflowY: node.scrollHeight > node.clientHeight + 1,',
107
- ' type: node.getAttribute("data-component-type"),',
108
- ' parentId: parent ? parent.getAttribute("data-element-id") : null,',
109
- ' parentIndex: parentIndex < 0 ? null : parentIndex,',
110
- ' surface: painted(style.backgroundColor) || bordered || style.boxShadow !== "none"',
111
- ' };',
112
- '});',
113
- '})()'
114
- ].join('\n');
115
- /** Mobil çizicinin hata sınırı işaretleri; betik düşerse boş (çekimi düşürmez). */
116
- async function readRenderErrors(page) {
117
- return page.evaluate(renderErrorsScript).then(parseRenderErrors).catch(() => []);
118
- }
119
- /**
120
- * F-09 — sayfa akışı yalnız tarayıcıda koşar, bu yüzden bir setter iteratee'sinin
121
- * patlamasının gateway'e ulaşan HİÇBİR yolu yoktu: akış 200 dönüyor, hedef prop
122
- * yazılmıyor ve tek belirti yığında kaybolan bir bildirim oluyordu. Runtime artık
123
- * yapısal hata defterini bu global anahtarda yayınlıyor (`@ng-apps/flow` →
124
- * flow-runtime-errors.ts); kare çekilirken bedavaya okunur.
125
- */
126
- export const flowRuntimeErrorsScript = 'Array.isArray(window.__ngAppsFlowRuntimeErrors) ? window.__ngAppsFlowRuntimeErrors.slice(0, 50) : []';
127
- const maxCaptureHeight = 16384;
128
- const navigationTimeoutMs = 20000;
129
- const apiResponseTimeoutMs = 15000;
130
- const settleMs = 600;
131
- /** Akış bittikten sonra Svelte'in son render'ına bırakılan pay. */
132
- const flowSettleGraceMs = 250;
133
- const flowPollMs = 120;
134
- const idleCloseMs = 60000;
135
- /** Etkileşim adımındaki `wait` üst sınırı; delayMs tavanıyla aynı çizgide. */
136
- const maxActionWaitMs = 15000;
137
- /** Viewport büyütmesinden sonra yeniden yerleşme için tanınan üst süre (B-4). */
138
- const resizeSettleBudgetMs = 3000;
139
- /**
140
- * Navigasyon zaman aşımında toplam deneme sayısı. Soğuk Studio'da ilk `page.goto` Vite
141
- * derlemesini ISITIR; ikinci deneme neredeyse her zaman geçer (09-08 S-8: aynı kare üç
142
- * çağrıya ve ~3.5 dakikaya mal oldu). İkiden fazlası turu uzatır, bir şey kazandırmaz.
143
- */
144
- const navigationAttempts = 2;
145
- /**
146
- * Yalnız zaman aşımı yeniden denenir. `net::ERR_CONNECTION_REFUSED` / DNS gibi hızlı düşen
147
- * hatalarda Studio ayakta değildir; tekrar yalnız bekleme süresini iki katına çıkarır ve
148
- * çağırana yanlış ipucu verir.
149
- */
150
- function isNavigationTimeout(message) {
151
- return /timeout/i.test(message);
152
- }
153
- async function measureContentHeight(page) {
154
- const measured = await page.evaluate(contentHeightScript).catch(() => undefined);
155
- const height = measured?.contentHeight;
156
- return typeof height === 'number' && Number.isFinite(height) && height > 0 ? Math.ceil(height) : undefined;
157
- }
158
- /**
159
- * Kırpma kutusunu çözer. Bulunamayan ya da çizilmemiş düğüm BOŞ KARE değil HATA döndürür:
160
- * "yan sütunu kırptım" deyip bomboş bir PNG almak sessiz yanlıştır.
161
- */
162
- async function resolveClip(page, clip) {
163
- if (!('element' in clip))
164
- return clip;
165
- const raw = await page.evaluate(clipRectScript(clip.element)).catch(() => null);
166
- if (raw === null || raw === undefined) {
167
- throw new PlatformApiError(`page_screenshot: clip.element '${clip.element}' sayfada yok — kırpılacak kutu bulunamadı. elementId'leri page_schema_get ile doğrulayın; bir komponentin İÇ kontrolünü kırpıyorsanız <elementId>__<rol> biçimini kullanın.`);
168
- }
169
- if (!(raw.width > 0 && raw.height > 0)) {
170
- throw new PlatformApiError(`page_screenshot: clip.element '${clip.element}' ÇİZİLMEMİŞ (${Math.round(raw.width)}x${Math.round(raw.height)} px) — kırpma boş kare üretirdi. Düğüm gizli ya da sıfır boyutlu olabilir; layoutReport: true ile kutusuna bakın.`);
171
- }
172
- return { x: Math.round(raw.x), y: Math.round(raw.y), width: Math.round(raw.width), height: Math.round(raw.height) };
173
- }
174
- /**
175
- * Playwright bağlamı ve tarayıcısı bu sınıfta PAYLAŞILIR. İki çekim aynı anda koştuğunda
176
- * 2026-09-06 koşusunda ikisi de `Target page, context or browser has been closed` ile düştü
177
- * (çağrı #204/#205, 64 sn kayıp, ajan yeniden giriş yapmak zorunda kaldı — `S-3`). Kök neden
178
- * iki yerdeydi: (1) paylaşılan tarayıcı üstünde eşzamanlı bağlam açma, (2) her çekimin `finally`
179
- * bloğunda ÖNCEKİ boşta-kapatma zamanlayıcısı temizlenmeden yenisinin kurulması — kaçan
180
- * zamanlayıcı sonraki çekimin ortasında tarayıcıyı kapatıyordu.
181
- *
182
- * Çözüm bir seri kuyruk: çekimler tek tek koşar, bekleyen çağrı `queuedMs` ile ne kadar
183
- * beklediğini söyler. Sıcak tarayıcı havuzu bu işin dışıdır (Ç2 · tur ekonomisi).
184
- */
185
- export class StudioScreenshotter {
186
- config;
187
- launcher;
188
- browserPromise;
189
- idleTimer;
190
- queue = Promise.resolve();
191
- pending = 0;
192
- recycleNext = false;
193
- constructor(config, launcher) {
194
- this.config = config;
195
- this.launcher = launcher;
196
- }
197
- /**
198
- * Kuyruk sınırsız birikmesin: bekleme üst sınırı aşılırsa çağrı tarayıcıya HİÇ girmeden
199
- * anlaşılır bir hatayla döner ve sırasını öndekine zincirleyip çıkar, böylece kuyruğun geri
200
- * kalanı serileşmiş kalır (Kısıt: tek yavaş çekimin arkasında kuyruk şişmemeli).
201
- */
202
- async capture(request) {
203
- const queuedAt = Date.now();
204
- // Sayaç SENKRON artar: `Promise.all` ile aynı turda gelen iki çağrıda ikincisi beklediğini
205
- // ancak böyle bilebilir (async bir bayrak o turda henüz kurulmamış olurdu).
206
- const waited = this.pending > 0;
207
- const previous = this.queue;
208
- this.pending += 1;
209
- let release;
210
- this.queue = new Promise(resolve => {
211
- release = resolve;
212
- });
213
- let timer;
214
- const capExceeded = await Promise.race([
215
- previous.then(() => false, () => false),
216
- new Promise(resolve => {
217
- timer = setTimeout(() => resolve(true), this.config.screenshotQueueWaitMs);
218
- timer.unref?.();
219
- })
220
- ]);
221
- if (timer)
222
- clearTimeout(timer);
223
- if (capExceeded) {
224
- this.pending -= 1;
225
- void previous.then(release, release);
226
- 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);
227
- }
228
- const queuedMs = Date.now() - queuedAt;
229
- const recycled = this.recycleNext;
230
- this.recycleNext = false;
231
- if (waited)
232
- logStep('tarayıcı ⏸', `çekim kuyrukta ${queuedMs} ms bekledi — eşzamanlı çekimler seri koşar`);
233
- try {
234
- const result = await this.captureOne(request);
235
- return {
236
- ...result,
237
- ...(waited ? { queuedMs: Math.max(queuedMs, 1) } : {}),
238
- ...(recycled ? { contextRecycled: true } : {})
239
- };
240
- }
241
- catch (error) {
242
- // Bağlam/tarayıcı bozulduysa bir sonraki çağrı TEMİZ bir tarayıcıyla başlasın; hata
243
- // kuyruktaki komşuya bulaşmasın.
244
- if (/browser has been closed|target (page|closed)|context.*closed/i.test(error instanceof Error ? error.message : String(error))) {
245
- this.recycleNext = true;
246
- const expired = this.browserPromise;
247
- this.browserPromise = undefined;
248
- expired?.then(instance => instance.close()).catch(() => undefined);
249
- }
250
- throw error;
251
- }
252
- finally {
253
- this.pending -= 1;
254
- release();
255
- }
256
- }
257
- async launch() {
258
- if (this.launcher)
259
- return this.launcher();
260
- let chromium;
261
- try {
262
- chromium = (await import('playwright-core')).chromium;
263
- }
264
- catch {
265
- throw new PlatformApiError('playwright-core paketi yüklü değil — zeyrek-mcp dizininde `npm install playwright-core` çalıştırın, ardından tarayıcı için `npx playwright-core install chromium`.');
266
- }
267
- logStep('tarayıcı →', 'chromium başlatılıyor (ilk çağrı — sonraki çağrılar önbellekteki tarayıcıyı kullanır)');
268
- try {
269
- return await chromium.launch({
270
- headless: true,
271
- ...(this.config.screenshotBrowserPath ? { executablePath: this.config.screenshotBrowserPath } : {})
272
- });
273
- }
274
- catch (firstError) {
275
- try {
276
- return await chromium.launch({ headless: true, channel: 'chrome' });
277
- }
278
- catch {
279
- const message = firstError instanceof Error ? firstError.message : String(firstError);
280
- throw new PlatformApiError(`Chromium başlatılamadı: ${message}. \`npx playwright-core install chromium\` ile indirin; alternatif olarak sistemdeki tarayıcının yolunu SCREENSHOT_BROWSER_PATH ile verin (alpine imajında \`apk add chromium\` + /usr/bin/chromium-browser).`);
281
- }
282
- }
283
- }
284
- /**
285
- * Bekleme stratejisi bilinçli olarak `networkidle` DEĞİL: Studio'nun kök layout'u
286
- * office-lite.zeyrek.dev'den dış bir script yükler ve ağ hiç durulmayabilir. SSR kabuğu yalnız
287
- * spinner bastığı için domcontentloaded da tek başına yetmez. Bunun yerine sayfanın kendi veri
288
- * isteği (`/{app}/p/{slug}`) gözlenir; yanıt hiç gelmezse bu hata SAYILMAZ — renderWaitTimedOut
289
- * işaretlenir ve kare yine çekilir, çünkü bozuk/boş sayfayı görmek de bir doğrulamadır. Veri
290
- * yanıtından sonra fontlar + kısa yerleşme beklemesi Svelte hydrate'ine alan bırakır.
291
- */
292
- async runActions(page, request, consoleLog) {
293
- if (!request.actions || request.actions.length === 0)
294
- return undefined;
295
- const outcomes = [];
296
- for (const [index, action] of request.actions.entries()) {
297
- if (action.action === 'wait') {
298
- await page.waitForTimeout(Math.min(Math.max(Number(action.ms ?? settleMs), 0), maxActionWaitMs));
299
- outcomes.push({ index, action: 'wait', found: true, applied: true, requests: 0 });
300
- continue;
301
- }
302
- const snapshotBefore = parseReadiness(await page.evaluate(readinessScript).catch(() => undefined));
303
- const activityBefore = snapshotBefore?.activity ?? null;
304
- const raw = await page.evaluate(actionScript(action, request.target)).catch(error => ({
305
- found: false,
306
- applied: false,
307
- note: error instanceof Error ? error.message : String(error),
308
- requests: 0
309
- }));
310
- const before = (raw ?? {});
311
- // Hedef bulunamadıysa ajan neyi yanlış yazdığını KAREDEN çıkaramaz. `__` taşıyan hedef bir
312
- // iç rol denemesidir: bilinen roller sayılır. Kök hedefi bulunamadıysa sorun elementId'nin
313
- // kendisindedir, şemayı okumaya yönlendirilir (P1-3 Step 3).
314
- if (before.found === false && !before.note && typeof action.target === 'string') {
315
- before.note = action.target === '__tabs' || action.target.startsWith(`__tabs${INTERNAL_ROLE_SEPARATOR}`)
316
- ? `'${action.target}' sayfada yok: bu sayfa sekme çubuğunda değil ya da uygulamada sekmeler kapalı (application_get → properties.mobile.tabs). Sekme öğesinin kimliği __tabs__item-<sayfa slug'ı>.`
317
- : action.target.includes(INTERNAL_ROLE_SEPARATOR)
318
- ? `'${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.`
319
- : `'${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).`;
320
- }
321
- // Olay akışı uç çağırıyor: yerleşme beklemeden sonraki istek sayısını ölçmek "istek gitti mi"
322
- // sorusunu cevaplar — koşuda kusuru kesinleştiren gözlem buydu.
323
- //
324
- // Bekleme ana yükleme döngüsüyle AYNI hazırlık kararıdır: akış koştuysa bitince, hiç akış
325
- // tetiklenmediyse (metin alanına `set`) DOM durulunca, eylem sayfayı değiştirdiyse yeni
326
- // sayfa hazır olunca çıkar. Eskiden akış tetiklemeyen her eylem `settleMs + delayMs`'in
327
- // tamamını bekliyordu: 09-06·2 koşusunda üç eylemli giriş denemesi 47 sn (B-6).
328
- const readiness = await this.waitForReady(page, {
329
- budgetMs: apiResponseTimeoutMs + request.delayMs,
330
- baselineUrl: snapshotBefore?.href ?? request.url,
331
- completedBefore: activityBefore?.completed ?? Number.POSITIVE_INFINITY
332
- });
333
- if (readiness.state === 'not-ready' && request.captureUnready !== true) {
334
- const failure = consoleLog ? describePageFailure(consoleLog, await readRenderErrors(page)) : '';
335
- 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)}.${failure} 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);
336
- }
337
- const after = await page.evaluate("performance.getEntriesByType('resource').length").catch(() => undefined);
338
- const requests = typeof after === 'number' && typeof before.requests === 'number' ? Math.max(after - before.requests, 0) : 0;
339
- // Yerel bir akış (yalnız prop yazan setter) hiç uca gitmez ama işini YAPAR. Sayaç bu ayrımı
340
- // veren tek kanıttır; sayaç yoksa flowRuns bilinmez kalır. Eylem sayfayı değiştirdiyse
341
- // yeni sayfanın sayacı sıfırdan başlar, orada biten her akış bu eyleme yazılır.
342
- const activityAfter = readiness.snapshot?.activity ?? null;
343
- const navigated = snapshotBefore !== undefined && readiness.snapshot !== undefined && runtimePath(readiness.snapshot.href) !== runtimePath(snapshotBefore.href);
344
- const flowRuns = activityAfter === null
345
- ? undefined
346
- : navigated
347
- ? activityAfter.completed
348
- : activityBefore === null ? undefined : Math.max(activityAfter.completed - activityBefore.completed, 0);
349
- // `tab` adımının KANITI tıklama değil, sonuçta hangi sekmenin açık olduğudur: bileşen
350
- // seçimi kendi prop'una yazmadıysa kare hâlâ eski sekmeyi gösterir ve rapor bunu söyler.
351
- const activeTab = action.action === 'tab' && typeof action.target === 'string'
352
- ? await page.evaluate(activeTabScript(action.target)).catch(() => undefined)
353
- : undefined;
354
- // G-14 — DOM'a yazmak değerin prop'a ulaştığını kanıtlamaz. İz yalnız okuyucu yayınlanıyor ve
355
- // makine düğümü tanıyorsa (dize) karşılaştırılır; aksi hâlde propWritten bilinmez kalır.
356
- const propsAfter = typeof before.propsBefore === 'string' && before.applied === true && !navigated
357
- ? await page.evaluate(runtimePropsScript(rootElementId(String(action.target ?? '')))).catch(() => undefined)
358
- : undefined;
359
- const propWritten = typeof propsAfter === 'string' ? propsAfter !== before.propsBefore : undefined;
360
- const applied = before.applied === true && propWritten !== false;
361
- const note = propWritten === false
362
- ? `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).`
363
- : before.note;
364
- outcomes.push({
365
- index,
366
- action: String(action.action),
367
- ...(action.target === undefined ? {} : { target: action.target }),
368
- ...(action.to === undefined ? {} : { to: action.to }),
369
- found: before.found === true,
370
- applied,
371
- ...(propWritten === undefined ? {} : { propWritten }),
372
- ...(note === undefined ? {} : { note }),
373
- requests,
374
- ...(flowRuns === undefined ? {} : { flowRuns }),
375
- ...(before.matchedBy === undefined ? {} : { matchedBy: before.matchedBy }),
376
- ...(typeof activeTab === 'string' ? { activeTab } : {}),
377
- ...(before.optionsSeen === undefined ? {} : { optionsSeen: before.optionsSeen }),
378
- ...(before.waitedMs === undefined ? {} : { waitedMs: before.waitedMs }),
379
- ...(before.submitVia === undefined ? {} : { submitVia: before.submitVia }),
380
- pageState: readiness.state
381
- });
382
- }
383
- logStep('tarayıcı ⤵', `${outcomes.length} etkileşim adımı koştu`, outcomes.map(outcome => `${outcome.index}. ${outcome.action}${outcome.target ? ` '${outcome.target}'` : ''}${outcome.to ? ` ⇒ '${outcome.to}'` : ''} → ${outcome.found ? (outcome.applied ? 'uygulandı' : 'bulundu ama uygulanamadı') : 'hedef BULUNAMADI'} · ${outcome.requests} ağ isteği`));
384
- return outcomes;
385
- }
386
- /**
387
- * Sayfa hazır olana kadar yoklar; hazır kararını `decideReadiness` verir. Karar verildikten
388
- * 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ı
389
- * yönlendirmeyse "bitti" sinyali yeni sayfa açılırken gelir ve doğrulama olmadan kare o
390
- * geçişte alınırdı. Adres değiştiyse sayaç yeni sayfada sıfırdan başlar; baz da sıfırlanır.
391
- *
392
- * Döngü hem süreyle hem TUR sayısıyla sınırlıdır: sahte saatle koşan testte `Date.now()`
393
- * ilerlemez ve yalnız süreye bakan bir döngü hiç bitmezdi.
394
- */
395
- async waitForReady(page, options) {
396
- const startedAt = Date.now();
397
- const deadline = startedAt + options.budgetMs;
398
- const maxPolls = Math.ceil(options.budgetMs / flowPollMs) + 1;
399
- let completedBefore = options.completedBefore;
400
- let previousKey;
401
- let quiet = 0;
402
- let last;
403
- for (let poll = 0; poll < maxPolls; poll++) {
404
- const snapshot = parseReadiness(await page.evaluate(readinessScript).catch(() => undefined));
405
- if (snapshot === undefined) {
406
- // Yönlendirme sırasında evaluate düşer: sessizlik sayacı sıfırlanır, yoklama sürer.
407
- previousKey = undefined;
408
- quiet = 0;
409
- }
410
- else {
411
- last = snapshot;
412
- if (runtimePath(snapshot.href) !== runtimePath(options.baselineUrl))
413
- completedBefore = 0;
414
- const key = JSON.stringify(snapshot);
415
- quiet = key === previousKey ? quiet + 1 : 0;
416
- previousKey = key;
417
- const state = decideReadiness(snapshot, quiet, completedBefore, options.baselineUrl);
418
- if (state !== undefined) {
419
- await page.waitForTimeout(flowSettleGraceMs);
420
- const confirmed = parseReadiness(await page.evaluate(readinessScript).catch(() => undefined));
421
- if (confirmed !== undefined && !confirmed.skeleton && confirmed.href === snapshot.href && (confirmed.activity === null || confirmed.activity.running === 0)) {
422
- return { state, waitedMs: Date.now() - startedAt, snapshot: confirmed };
423
- }
424
- previousKey = undefined;
425
- quiet = 0;
426
- }
427
- }
428
- if (Date.now() >= deadline || poll === maxPolls - 1)
429
- break;
430
- await page.waitForTimeout(flowPollMs);
431
- }
432
- return { state: 'not-ready', waitedMs: Date.now() - startedAt, ...(last === undefined ? {} : { snapshot: last }) };
433
- }
434
- /**
435
- * Viewport büyütmesinden sonra sayfanın YENİDEN YERLEŞMESİNİ bekler. Eski sırada (ölç → büyüt →
436
- * eylemler) büyütme sonrası render'ı her eylemin kendi hazırlık döngüsü soğuruyordu; sıra
437
- * eylemlerden sonraya alınınca geriye yalnız 600 ms kalmıştı ve `onResize`'a bağlı ya da
438
- * sanallaştırılmış bir tablo o pay içinde yeni satırlarını çizmeyebilirdi — özet ve kare
439
- * büyütme ÖNCESİ gerçeği anlatırdı (B-4).
440
- */
441
- async settleAfterResize(page, baselineUrl) {
442
- await page.waitForTimeout(settleMs);
443
- // Yeni akış beklenmiyor: karar DOM durulmasından çıkar (`ready-no-flows` / `ready-no-counter`).
444
- await this.waitForReady(page, { budgetMs: resizeSettleBudgetMs, baselineUrl, completedBefore: Number.POSITIVE_INFINITY });
445
- }
446
- async captureOne(request) {
447
- const startedAt = Date.now();
448
- if (this.idleTimer)
449
- clearTimeout(this.idleTimer);
450
- this.browserPromise ??= this.launch();
451
- let browser = await this.browserPromise.catch(error => {
452
- this.browserPromise = undefined;
453
- throw error;
454
- });
455
- let context;
456
- // Donuk kare (plans/mobil-animasyon-sistemi A6): sonsuz döngülü sayfanın iki karesi hiç aynı
457
- // çıkmıyordu. Runtime "hareketi azalt"ı dondurma sebebi sayar ve onu matchMedia'dan canlı okur
458
- // (A10); tercih bağlamda verilir ki ilk kareden itibaren geçerli olsun.
459
- const contextOptions = {
460
- viewport: request.viewport,
461
- deviceScaleFactor: (request.dpr ?? 1) * (request.scale ?? 1),
462
- ...(request.motion === 'play' ? {} : { reducedMotion: 'reduce' })
463
- };
464
- try {
465
- context = await browser.newContext(contextOptions);
466
- }
467
- catch {
468
- this.browserPromise = this.launch();
469
- browser = await this.browserPromise.catch(error => {
470
- this.browserPromise = undefined;
471
- throw error;
472
- });
473
- context = await browser.newContext(contextOptions);
474
- }
475
- 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' : ''}`);
476
- try {
477
- const cookies = new Map();
478
- if (request.token)
479
- cookies.set('NG_AccessToken', request.token);
480
- for (const cookie of request.cookies ?? [])
481
- cookies.set(cookie.name, cookie.value);
482
- if (cookies.size > 0) {
483
- await context.addCookies([...cookies].map(([name, value]) => ({ name, value, url: request.cookieUrl, httpOnly: false })));
484
- }
485
- // Tema tercihi çerezde değil localStorage'da yaşar ve tema store'u değerini modül İLK
486
- // değerlendirildiğinde okur: sayfa açıldıktan sonra yazmak bir sonraki yüklemeye kadar
487
- // etkisizdir. Bu yüzden tercih, sayfanın script'lerinden önce koşan bağlam script'iyle
488
- // ekilir. Yeteneği olmayan bağlamda çağrı SESSİZCE açık temada kare almaz, hata verir.
489
- if (request.colorScheme) {
490
- if (!context.addInitScript) {
491
- throw new PlatformApiError(`colorScheme='${request.colorScheme}' uygulanamadı: tarayıcı bağlamı addInitScript desteklemiyor. Tema tercihi sayfa açılmadan ekilmek zorunda; desteksiz bağlamda kare projenin varsayılan modunda çıkardı ve bu sessizce yanlış olurdu.`);
492
- }
493
- await context.addInitScript(`try { localStorage.setItem(${JSON.stringify(VIEWER_MODE_STORAGE_KEY)}, ${JSON.stringify(request.colorScheme)}) } catch (error) { /* localStorage kapalıysa proje varsayılanı çizilir */ }`);
494
- }
495
- const page = await context.newPage();
496
- // Konsol ve istisna dinleyicileri navigasyondan ÖNCE takılır: modül yüklenirken patlayan
497
- // bileşenin hatası ilk karede düşer ve sonradan takılan dinleyici onu kaçırırdı (g-06).
498
- const consoleLog = createPageConsoleLog();
499
- page.on?.('console', message => consoleLog.onConsole(message));
500
- page.on?.('pageerror', error => consoleLog.onPageError(error));
501
- // Soğuk Vite derlemesinde sayfanın kendi veri isteği 15 sn'yi aşabiliyor; delayMs bu
502
- // bütçeyi de uzatır (09-02 E-2). Yanıt hazırlık döngüsünü BLOKLAMAZ: runtime dışına
503
- // yönlendirilen sayfa bu isteği hiç atmaz ve eskiden 15 sn boşa bekleniyordu.
504
- const readyBudgetMs = apiResponseTimeoutMs + request.delayMs;
505
- const watchApiResponse = () => page
506
- .waitForResponse(response => response.url().includes(request.apiPathMarker), { timeout: readyBudgetMs })
507
- .then(response => response.status(), () => undefined);
508
- // `delayMs` navigasyon bütçesini de uzatır. Eskiden sabit 20 sn'ydi ve soğuk Studio'da
509
- // ajanın elinde tek çıkış çağrıyı tekrarlamaktı (09-08 S-8) — "sabrım bu kadar" demenin
510
- // tek anlamı olsun diye iki bütçe de aynı düğmeye bağlı.
511
- const navigationTimeout = navigationTimeoutMs + request.delayMs;
512
- let observedApiStatus = watchApiResponse();
513
- let navigationRetried = false;
514
- for (let attempt = 1;; attempt++) {
515
- try {
516
- await page.goto(request.url, { waitUntil: 'domcontentloaded', timeout: navigationTimeout });
517
- break;
518
- }
519
- catch (error) {
520
- const message = error instanceof Error ? error.message : String(error);
521
- const timedOut = isNavigationTimeout(message);
522
- if (!timedOut || attempt >= navigationAttempts) {
523
- const attemptNote = attempt > 1 ? ` (${attempt} deneme)` : '';
524
- const budgetNote = timedOut
525
- ? ` 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.`
526
- : '';
527
- 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\`)`);
528
- }
529
- navigationRetried = true;
530
- // Veri isteği gözlemi de sıfırlanır: ilk denemeyi bekleyen sayaç çoktan dolmuş olur ve
531
- // yeniden denenen navigasyonun yanıtını kaçırıp boş loader karesi damgalardı.
532
- observedApiStatus = watchApiResponse();
533
- logStep('tarayıcı ⟳', `navigasyon ${navigationTimeout} ms'de düştü, bir kez daha deneniyor — ilk deneme soğuk derlemeyi ısıttı`);
534
- }
535
- }
536
- const readiness = await this.waitForReady(page, { budgetMs: readyBudgetMs, baselineUrl: request.url, completedBefore: 0 });
537
- // Gözlendiyse dolu, hâlâ bekleniyorsa (yönlendirme, hata) undefined — ikinci vaat hemen çözülür.
538
- const apiStatus = await Promise.race([observedApiStatus, Promise.resolve(undefined)]);
539
- const apiNote = apiStatus === undefined ? 'sayfanın veri isteği gözlenmedi' : `veri isteği HTTP ${apiStatus}`;
540
- if (readiness.state === 'not-ready' && request.captureUnready !== true) {
541
- const failure = describePageFailure(consoleLog, await readRenderErrors(page));
542
- throw new PlatformApiError(`Sayfa ${readiness.waitedMs} ms içinde yüklenmedi, kare ALINMADI — ${describeUnready(readiness, apiNote)}.${failure} 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);
543
- }
544
- await page.evaluate('document.fonts.ready.then(() => true)').catch(() => undefined);
545
- const flowsSettled = readiness.state === 'ready' || readiness.state === 'ready-no-flows'
546
- ? true
547
- : readiness.state === 'not-ready' ? false : undefined;
548
- const finalHref = readiness.snapshot?.href;
549
- const finalUrl = finalHref !== undefined && runtimePath(finalHref) !== runtimePath(request.url) ? finalHref : undefined;
550
- const actions = await this.runActions(page, request, consoleLog);
551
- // Ölçüm EYLEMLERDEN SONRA: eylem sekme değiştirir, filtre uygular, panel açar — içerik
552
- // boyu o zaman kesinleşir. Eskiden ölçüm eylemlerden önceydi ve ikinci sekmede 40 satırlık
553
- // tablo açan bir çekim viewport boyunda kalıyordu; `contentHeight` ilk sekmenin boyuydu
554
- // (S-1, 2026-09-16 raporu madde 6). Yalnız YÜKSEKLİK büyür, genişlik sabittir: `hideOn`
555
- // kırılım noktaları ve duyarlı yerleşim kareyi çekerken kaymaz (K4).
556
- // Doku sınırı RASTER pikseldir: mobil kare @2-3 çekilir, CSS tavanı ölçekle bölünür (web'de
557
- // ölçek ≤ 1, tavan 16384 kalır).
558
- const heightLimit = Math.floor(maxCaptureHeight / Math.max(1, (request.dpr ?? 1) * (request.scale ?? 1)));
559
- const measured = request.fullPage ? await measureContentHeight(page) : undefined;
560
- const captureHeight = measured === undefined ? undefined : Math.min(measured, heightLimit);
561
- let viewportHeight = request.viewport.height;
562
- if (captureHeight !== undefined && captureHeight > viewportHeight) {
563
- await page.setViewportSize({ width: request.viewport.width, height: captureHeight });
564
- viewportHeight = captureHeight;
565
- await this.settleAfterResize(page, readiness.snapshot?.href ?? request.url);
566
- }
567
- // K7 — kırpma kutusu katlanmış alanın ALTINDAYSA kare boş çıkardı: viewport kutuyu
568
- // kapsayana kadar büyütülür (fullPage ile aynı yol) ve kutu YENİDEN okunur, çünkü
569
- // büyüyen viewport yerleşimi kaydırabilir.
570
- let clip;
571
- if (request.clip) {
572
- clip = await resolveClip(page, request.clip);
573
- if (clip.y + clip.height > viewportHeight) {
574
- const wanted = Math.ceil(clip.y + clip.height) + 8;
575
- if (wanted > heightLimit) {
576
- throw new PlatformApiError(`page_screenshot: kırpma kutusu ${Math.round(clip.y)}+${Math.round(clip.height)} px'te bitiyor ve Chromium'un ${maxCaptureHeight} px'lik doku sınırının${heightLimit === maxCaptureHeight ? '' : ` (bu ölçekte ${heightLimit} CSS px)`} altına sığmıyor — viewport o kadar büyütülemez ve kare kutunun tamamını gösteremezdi. Kutuyu elle daraltın ({ x, y, width, height }) ya da sayfayı bölerek doğrulayın.`);
577
- }
578
- const grown = Math.min(wanted, heightLimit);
579
- await page.setViewportSize({ width: request.viewport.width, height: grown });
580
- viewportHeight = grown;
581
- await this.settleAfterResize(page, readiness.snapshot?.href ?? request.url);
582
- clip = await resolveClip(page, request.clip);
583
- }
584
- }
585
- const layout = request.layoutReport === true
586
- ? await page.evaluate(layoutReportScript).then(value => Array.isArray(value) ? value : []).catch(() => [])
587
- : undefined;
588
- const ledger = await page.evaluate(flowRuntimeErrorsScript)
589
- .then(value => Array.isArray(value) ? value : [])
590
- .catch(() => []);
591
- const { flowErrors: runtimeErrors, expressionErrors } = splitExpressionErrors(ledger);
592
- const renderErrors = await readRenderErrors(page);
593
- const consoleErrors = consoleLog.consoleErrors(record => isExpressionEcho(expressionErrors, record.text));
594
- const pageErrors = consoleLog.pageErrors();
595
- // Özet kareden ÖNCE ve büyütmeden SONRA: ikisi de aynı yerleşmiş sayfayı anlatsın.
596
- const summaryRaw = request.summary === false
597
- ? undefined
598
- : await page.evaluate(domSummaryScript(request.viewport.height)).then(value => ({ value })).catch(error => ({ error: error instanceof Error ? error.message : String(error) }));
599
- const summary = summaryRaw && 'value' in summaryRaw ? parseDomSummary(summaryRaw.value) : undefined;
600
- const summaryUnavailable = summaryRaw === undefined
601
- ? undefined
602
- : 'error' in summaryRaw
603
- ? summaryRaw.error
604
- : summary === undefined ? 'tarayıcı özet döndürmedi (beklenmeyen biçim)' : undefined;
605
- // Olgu doğrulamasında kare HİÇ çekilmez: maliyetin tamamı buradadır (S-5).
606
- const format = request.format ?? 'png';
607
- const png = request.image === 'none'
608
- ? undefined
609
- : await page.screenshot({
610
- type: format,
611
- ...(format === 'jpeg' && request.quality !== undefined ? { quality: request.quality } : {}),
612
- fullPage: false,
613
- ...(clip === undefined ? {} : { clip })
614
- });
615
- const elapsedMs = Date.now() - startedAt;
616
- logStep('tarayıcı ←', `${png === undefined ? 'kare YOK (image:none)' : `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}`}`);
617
- if (runtimeErrors.length > 0) {
618
- 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 ?? ''}`));
619
- }
620
- return {
621
- ...(png === undefined ? {} : { png, mimeType: format === 'jpeg' ? 'image/jpeg' : 'image/png' }),
622
- ...(png === undefined ? {} : (size => size === undefined ? {} : { imageSize: size })(imageSize(png))),
623
- ...(clip === undefined ? {} : { clip }),
624
- ...(summary === undefined ? {} : { summary }),
625
- ...(summaryUnavailable === undefined ? {} : { summaryUnavailable }),
626
- apiStatus,
627
- renderWaitTimedOut: apiStatus === undefined,
628
- elapsedMs,
629
- pageState: readiness.state,
630
- readyAfterMs: readiness.waitedMs,
631
- ...(finalUrl === undefined ? {} : { finalUrl }),
632
- ...(measured === undefined ? {} : { contentHeight: measured }),
633
- ...(measured !== undefined && measured > heightLimit ? { fullPageTruncated: true } : {}),
634
- ...(layout === undefined ? {} : { layout }),
635
- ...(actions === undefined ? {} : { actions }),
636
- ...(runtimeErrors.length > 0 ? { runtimeErrors } : {}),
637
- ...(expressionErrors.length > 0 ? { expressionErrors } : {}),
638
- ...(consoleErrors.length > 0 ? { consoleErrors } : {}),
639
- ...(pageErrors.length > 0 ? { pageErrors } : {}),
640
- ...(renderErrors.length > 0 ? { renderErrors } : {}),
641
- ...(flowsSettled === undefined ? {} : { flowsSettled }),
642
- ...(navigationRetried ? { navigationRetried: true } : {})
643
- };
644
- }
645
- finally {
646
- await context.close().catch(() => undefined);
647
- // Zamanlayıcı ÖNCE temizlenir: eskiden her çekim yenisini kurup eskisini bırakıyordu ve
648
- // kaçan zamanlayıcı sonraki çekimin ortasında tarayıcıyı kapatıyordu (S-3).
649
- if (this.idleTimer)
650
- clearTimeout(this.idleTimer);
651
- this.idleTimer = setTimeout(() => {
652
- const expired = this.browserPromise;
653
- this.browserPromise = undefined;
654
- expired?.then(instance => instance.close()).catch(() => undefined);
655
- }, idleCloseMs);
656
- this.idleTimer.unref?.();
657
- }
658
- }
659
- }