@appaflytech/zeyrek-ai-mcp 0.0.51 → 0.0.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/knowledge/componentCatalog.js +3 -1
- package/dist/knowledge/componentEvents.js +13 -1
- package/dist/knowledge/expressionPrelude.js +3 -0
- package/dist/knowledge/mobileComponentCatalog.js +4 -1
- package/dist/knowledge/mobileStore.js +258 -1
- package/dist/knowledge/normalize.js +4 -3
- package/dist/knowledge/platform/mobile.js +1 -1
- package/dist/knowledge/platform/mobileRules.js +79 -0
- package/dist/knowledge/rules.js +199 -6
- package/dist/knowledge/storeImport.js +69 -0
- package/dist/platform/argumentAliases.js +13 -4
- package/dist/platform/bodyFile.js +195 -38
- package/dist/platform/capture/actions.js +24 -5
- package/dist/platform/capture/capture.js +80 -11
- package/dist/platform/capture/persistent-browser.js +23 -1
- package/dist/platform/capture/readiness.js +32 -1
- package/dist/platform/errors.js +45 -5
- package/dist/platform/http.js +17 -9
- package/dist/platform/responses.js +7 -1
- package/dist/platform/secrets.js +8 -0
- package/dist/platform/token.js +32 -21
- package/dist/resources/examples/flow-credit-spend.json +233 -0
- package/dist/resources/examples/flow-purchase-paywall.json +662 -0
- package/dist/resources/examples/mobile-offline-list.json +1 -1
- package/dist/resources/examples/mobile-offline-queue.json +1 -1
- package/dist/resources/examples/mobile-offline-seamless.json +12 -3
- package/dist/resources/examples/store-import-mapping.json +25 -0
- package/dist/resources/guides/deployment.md +9 -0
- package/dist/resources/guides/expressions.md +1 -0
- package/dist/resources/guides/microservice.md +13 -1
- package/dist/resources/guides/mobile-publish.md +67 -2
- package/dist/resources/guides/mobile-steps.md +91 -3
- package/dist/resources/guides/mobile.md +38 -8
- package/dist/resources/guides/page.md +39 -8
- package/dist/resources/guides/translation.md +3 -1
- package/dist/resources/guides/workflow.md +30 -0
- package/dist/resources/schema-fingerprint.json +2 -2
- package/dist/resources/schemas/CreateWorkflowCommand.json +6 -1
- package/dist/resources/schemas/GlobalFunctionUpdateResultDto.json +4 -0
- package/dist/resources/schemas/PreviewWorkflowCommand.json +6 -1
- package/dist/resources/schemas/StepDto.json +6 -1
- package/dist/resources/schemas/UpdateWorkflowCommand.json +6 -1
- package/dist/server.js +4 -0
- package/dist/tools/applications.js +7 -2
- package/dist/tools/deployments.js +1 -1
- package/dist/tools/designSchemaPatch.js +7 -2
- package/dist/tools/environments.js +40 -0
- package/dist/tools/flows.js +102 -9
- package/dist/tools/mobile.js +56 -0
- package/dist/tools/mobilePurchases.js +236 -0
- package/dist/tools/mobileStore.js +2 -2
- package/dist/tools/mobileStoreImport.js +173 -0
- package/dist/tools/pages.js +45 -11
- package/dist/tools/platformInfo.js +4 -4
- package/dist/tools/screenWrite.js +107 -1
- package/dist/tools/variables.js +1 -1
- package/package.json +1 -1
- package/dist/resources/guides/mobile-flow.md +0 -64
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { open, realpath } from 'node:fs/promises';
|
|
1
|
+
import { open, realpath, stat } from 'node:fs/promises';
|
|
2
2
|
import { constants } from 'node:fs';
|
|
3
|
-
import { extname, resolve, sep } from 'node:path';
|
|
3
|
+
import { dirname, extname, isAbsolute, resolve, sep } from 'node:path';
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
import { PlatformApiError } from './errors.js';
|
|
6
6
|
/**
|
|
@@ -27,77 +27,234 @@ export const BODY_FILE_ARGUMENTS = {
|
|
|
27
27
|
/** `ops`/`operations` eşanlamlıdır (`microOps`); ikisinden biri satır içinde varsa gövde verilmiştir. */
|
|
28
28
|
const synonyms = { ops: ['ops', 'operations'], operations: ['ops', 'operations'] };
|
|
29
29
|
const maxBodyFileBytes = 5 * 1024 * 1024;
|
|
30
|
+
/**
|
|
31
|
+
* g-22 (D-22, K8 — `plans/defter-saha-bulgulari`): akış kodu JSON metninin içinde kaçışlı dizgi olarak duruyordu;
|
|
32
|
+
* Defter'de bir ters tırnak hatası düzenleri kırdı ve iş JS üreteçlerine taşındı. `{ "$file": "./adim.js" }` o
|
|
33
|
+
* dosyanın METNİYLE değiştirilir — gateway kodu ÇALIŞTIRMAZ (reddedilen seçenek: gateway'de .js koşturmak). Eklenen
|
|
34
|
+
* dosya bodyFile'ın bütün denetimlerinden geçer; sınırlar sahibin onayladığı K8 değerleridir.
|
|
35
|
+
*/
|
|
36
|
+
const includedExtensions = ['.js', '.mjs', '.ts', '.txt', '.sql', '.html', '.css'];
|
|
37
|
+
const maxIncludedFileBytes = 1024 * 1024;
|
|
38
|
+
const maxInclusions = 64;
|
|
30
39
|
export function bodyFileParam(argument) {
|
|
31
|
-
return z.string().min(1).optional().describe(`'${argument}' yerine okunacak yerel .json dosyası (en çok 5 MB): gövde ajan bağlamından geçmez, gateway dosyayı okuyup '${argument}' olarak aynı doğrulamadan geçirir. '${argument}' ile birlikte verilemez.
|
|
40
|
+
return z.string().min(1).optional().describe(`'${argument}' yerine okunacak yerel .json dosyası (en çok 5 MB): gövde ajan bağlamından geçmez, gateway dosyayı okuyup '${argument}' olarak aynı doğrulamadan geçirir. '${argument}' ile birlikte verilemez. Göreli yol .env'deki MCP_INPUT_DIR'e (tanımsızsa gateway'in çalışma dizinine) göre çözülür; dosya bu iki dizinden birinin altında olmalı (sembolik bağ çözülür). JSON'un herhangi bir yerine { "$file": "./adim.js" } yazılırsa o dosyanın METNİ oraya konur — kod çalıştırılmaz; yol bodyFile'ın klasörüne göre çözülür; ${includedExtensions.join(' ')}, UTF-8, dosya başına 1 MB, toplam 5 MB, en çok ${maxInclusions} ekleme; eklenen dosyanın içindeki $file işlenmez. Biçim tam { "$file": "<yol>" }'dur (tek anahtar, dizgi) — bozuk biçim ve satır içi gövdedeki $file istek gitmeden reddedilir. Akış code adımının args.value'su böylece kaçışsız düz JS dosyasında durur. Yanıtın bodyFile alanı okunan dosyanın mutlak yolunu, kökünü (root, rootSource) ve eklenen dosyaları (included: at, path, bytes) söyler.`);
|
|
32
41
|
}
|
|
33
42
|
export async function loadBodyFile(tool, args, config) {
|
|
34
43
|
const argument = BODY_FILE_ARGUMENTS[tool];
|
|
35
44
|
if (!argument || !args || typeof args !== 'object' || Array.isArray(args))
|
|
36
45
|
return { args };
|
|
37
46
|
const { bodyFile, ...rest } = args;
|
|
38
|
-
if (bodyFile === undefined)
|
|
39
|
-
return { args };
|
|
40
|
-
if (typeof bodyFile !== 'string' || bodyFile.length === 0)
|
|
41
|
-
return { args };
|
|
42
47
|
const inline = (synonyms[argument] ?? [argument]).filter(name => rest[name] !== undefined);
|
|
48
|
+
if (typeof bodyFile !== 'string' || bodyFile.length === 0) {
|
|
49
|
+
assertNoInlineInclusions(tool, inline, rest);
|
|
50
|
+
return { args };
|
|
51
|
+
}
|
|
43
52
|
if (inline.length > 0) {
|
|
44
53
|
throw new PlatformApiError(`${tool}: bodyFile ile '${inline.join("', '")}' birlikte verilemez — gövde ya dosyadan ya satır içinden gelir. Birini kaldırın. İstek gönderilmedi.`, 400);
|
|
45
54
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
55
|
+
// g-21 (D-21): göreli yol gateway sürecinin çalışma dizinine göre çözülüyordu ve ajan o dizini bilmez. Ajanın
|
|
56
|
+
// bildiği kök .env'deki MCP_INPUT_DIR'dir; tanımsızsa eski davranış (çalışma dizini) sürer.
|
|
57
|
+
const base = config.inputDirectory ? { source: 'MCP_INPUT_DIR', path: resolve(config.inputDirectory) } : { source: 'cwd', path: process.cwd() };
|
|
58
|
+
const roots = await allowedRoots(config);
|
|
59
|
+
const body = await readGuardedFile(tool, 'bodyFile', resolve(base.path, bodyFile), ['.json'], maxBodyFileBytes, roots, config);
|
|
60
|
+
let value;
|
|
61
|
+
try {
|
|
62
|
+
value = JSON.parse(body.buffer.toString('utf8'));
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
throw new PlatformApiError(`${tool}: bodyFile JSON değil — ${body.real} (${error instanceof Error ? error.message : String(error)}). İstek gönderilmedi.`, 400);
|
|
66
|
+
}
|
|
67
|
+
const included = await includeFiles(tool, value, dirname(body.real), body.buffer.length, roots, config);
|
|
68
|
+
const root = isAbsolute(bodyFile) ? body.root : { source: base.source, path: await realpath(base.path).catch(() => base.path) };
|
|
69
|
+
return {
|
|
70
|
+
args: { ...rest, [argument]: included.value },
|
|
71
|
+
note: {
|
|
72
|
+
path: body.real,
|
|
73
|
+
root: root.path,
|
|
74
|
+
rootSource: root.source,
|
|
75
|
+
argument,
|
|
76
|
+
bytes: body.buffer.length,
|
|
77
|
+
...(included.files.length > 0 ? { included: included.files } : {})
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/** MCP_INPUT_DIR önce gelir: dosya iki kökün de altındaysa yanıt ajanın bildiği kökü söyler. */
|
|
82
|
+
async function allowedRoots(config) {
|
|
83
|
+
const candidates = [
|
|
84
|
+
...(config.inputDirectory ? [{ source: 'MCP_INPUT_DIR', path: config.inputDirectory }] : []),
|
|
85
|
+
{ source: 'cwd', path: process.cwd() }
|
|
86
|
+
];
|
|
87
|
+
const resolved = await Promise.all(candidates.map(async (root) => ({ source: root.source, path: await realpath(resolve(root.path)).catch(() => undefined) })));
|
|
88
|
+
return resolved.filter((root) => root.path !== undefined);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* bodyFile'ın ve her `$file` eklemesinin ORTAK okuma kapısı: uzantı (yol ve bağın hedefi), izinli kök, düzenli dosya
|
|
92
|
+
* ve bayt sınırı. O_NONBLOCK FIFO'nun çağrıyı bekletmesini önler; boyut dosyanın büyüklüğüne değil okunan bayta bakar.
|
|
93
|
+
*
|
|
94
|
+
* `realpath` ile `open` atomik DEĞİLDİR: O_NOFOLLOW yalnız SON bileşenin sonradan bağa çevrilmesini önler, yolun
|
|
95
|
+
* ortasındaki bir klasör denetimden sonra dışarıyı gösteren bağa çevrilirse `open` dışarıdaki dosyayı açar (g-22
|
|
96
|
+
* incelemesi, `test/body-file-race.test.ts`). Açtıktan sonra tutamacın aygıt/düğümü yolun taze `stat`'ıyla
|
|
97
|
+
* karşılaştırılır ve `realpath` yeniden koşturulur: yol aynı yere çözülmüyor ya da izinli kökün dışına düşüyorsa okuma
|
|
98
|
+
* reddedilir. Bu pencereyi DARALTIR, kapatmaz — iki denetim arasında geri çevrilen bir bağ yine görülmez; tam çözüm
|
|
99
|
+
* (`openat2` RESOLVE_BENEATH) Node'da yoktur. Okunan bayt yine de açılan tutamaçtan gelir.
|
|
100
|
+
*/
|
|
101
|
+
async function readGuardedFile(tool, label, absolute, extensions, maxBytes, roots, config) {
|
|
102
|
+
const accepted = extensions.join(', ');
|
|
103
|
+
if (!extensions.includes(extname(absolute).toLowerCase())) {
|
|
104
|
+
throw new PlatformApiError(`${tool}: ${label} yalnız ${accepted} okur — verilen: ${absolute}. İstek gönderilmedi.`, 400);
|
|
49
105
|
}
|
|
50
106
|
let real;
|
|
51
107
|
try {
|
|
52
108
|
real = await realpath(absolute);
|
|
53
109
|
}
|
|
54
110
|
catch (error) {
|
|
55
|
-
throw new PlatformApiError(`${tool}:
|
|
111
|
+
throw new PlatformApiError(`${tool}: ${label} okunamadı — ${absolute} (${error instanceof Error ? error.message : String(error)}). Yol gateway sürecinin çalıştığı makineye göre çözülür. İstek gönderilmedi.`, 400);
|
|
56
112
|
}
|
|
57
|
-
if (extname(real).toLowerCase()
|
|
58
|
-
throw new PlatformApiError(`${tool}:
|
|
113
|
+
if (!extensions.includes(extname(real).toLowerCase())) {
|
|
114
|
+
throw new PlatformApiError(`${tool}: ${label} hedefi ${accepted} değil — ${real} (${absolute} bağının hedefi). İstek gönderilmedi.`, 400);
|
|
59
115
|
}
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const allowed = roots.some(root => root !== undefined && (real === root || real.startsWith(`${root}${sep}`)));
|
|
64
|
-
if (!allowed) {
|
|
65
|
-
throw new PlatformApiError(`${tool}: bodyFile izinli dizinlerin dışında — ${real}${real !== absolute ? ` (${absolute} bağının hedefi)` : ''}. Gateway yalnız çalışma dizininin (${process.cwd()}) ve .env'deki MCP_INPUT_DIR'in (${config.inputDirectory ?? 'tanımsız'}) altındaki dosyaları okur. İstek gönderilmedi.`, 400);
|
|
116
|
+
const root = roots.find(candidate => real === candidate.path || real.startsWith(`${candidate.path}${sep}`));
|
|
117
|
+
if (!root) {
|
|
118
|
+
throw new PlatformApiError(`${tool}: ${label} izinli dizinlerin dışında — ${real}${real !== absolute ? ` (${absolute} bağının hedefi)` : ''}. Gateway yalnız çalışma dizininin (${process.cwd()}) ve .env'deki MCP_INPUT_DIR'in (${config.inputDirectory ?? 'tanımsız'}) altındaki dosyaları okur. İstek gönderilmedi.`, 400);
|
|
66
119
|
}
|
|
67
|
-
// Denetim ve okuma AYNI tutamaçtan: yol denetimden sonra değişse de okunan, denetlenen dosyadır.
|
|
68
|
-
// O_NOFOLLOW son bileşenin sonradan bağa çevrilmesini, O_NONBLOCK `.json` adlı FIFO'nun çağrıyı
|
|
69
|
-
// bekletmesini önler; boyut dosyanın büyüklüğüne değil okunan bayta bakar.
|
|
70
120
|
let handle;
|
|
71
121
|
try {
|
|
72
122
|
handle = await open(real, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
73
123
|
}
|
|
74
124
|
catch (error) {
|
|
75
|
-
throw new PlatformApiError(`${tool}:
|
|
125
|
+
throw new PlatformApiError(`${tool}: ${label} açılamadı — ${real} (${error instanceof Error ? error.message : String(error)}). İstek gönderilmedi.`, 400);
|
|
76
126
|
}
|
|
77
|
-
let bytes;
|
|
78
|
-
let text;
|
|
79
127
|
try {
|
|
80
128
|
const info = await handle.stat();
|
|
81
129
|
if (!info.isFile()) {
|
|
82
|
-
throw new PlatformApiError(`${tool}:
|
|
130
|
+
throw new PlatformApiError(`${tool}: ${label} düzenli dosya değil — ${real}. İstek gönderilmedi.`, 400);
|
|
131
|
+
}
|
|
132
|
+
const fresh = await stat(real).catch(() => undefined);
|
|
133
|
+
const again = await realpath(absolute).catch(() => undefined);
|
|
134
|
+
const stillInside = again !== undefined && (again === root.path || again.startsWith(`${root.path}${sep}`));
|
|
135
|
+
if (!fresh || fresh.dev !== info.dev || fresh.ino !== info.ino || again !== real || !stillInside) {
|
|
136
|
+
throw new PlatformApiError(`${tool}: ${label} yolu denetimden sonra değişti — ${real} artık ${again ?? 'çözülemeyen bir yere'} gösteriyor (araya bir bağ girdi). Okunmadı. İstek gönderilmedi.`, 400);
|
|
83
137
|
}
|
|
84
|
-
const buffer = Buffer.alloc(
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
138
|
+
const buffer = Buffer.alloc(maxBytes + 1);
|
|
139
|
+
let bytesRead = 0;
|
|
140
|
+
// Tek `read` dosyanın tamamını döndürmek zorunda değildir; EOF'a ya da sınırın bir baytı ötesine kadar okunur.
|
|
141
|
+
while (bytesRead < buffer.length) {
|
|
142
|
+
const chunk = await handle.read(buffer, bytesRead, buffer.length - bytesRead, bytesRead);
|
|
143
|
+
if (chunk.bytesRead === 0)
|
|
144
|
+
break;
|
|
145
|
+
bytesRead += chunk.bytesRead;
|
|
89
146
|
}
|
|
90
|
-
|
|
147
|
+
if (bytesRead > maxBytes) {
|
|
148
|
+
throw new PlatformApiError(maxBytes === maxBodyFileBytes
|
|
149
|
+
? `${tool}: ${label} 5 MB sınırını aşıyor. Tanımı parçalara bölün — sayfa için page_schema_patch'e birden çok batch, akış için flow_patch. İstek gönderilmedi.`
|
|
150
|
+
: `${tool}: ${label} ${maxBytes / (1024 * 1024)} MB sınırını aşıyor — ${real}. Kodu birden çok dosyaya bölün. İstek gönderilmedi.`, 400);
|
|
151
|
+
}
|
|
152
|
+
return { real, root, buffer: buffer.subarray(0, bytesRead) };
|
|
91
153
|
}
|
|
92
154
|
finally {
|
|
93
155
|
await handle.close();
|
|
94
156
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
157
|
+
}
|
|
158
|
+
/** Kök JSON Pointer'ı boş dizgidir; mesajda `''` ile `/` (adı boş anahtar) karışmasın diye adıyla yazılır. */
|
|
159
|
+
function pointerLabel(at) {
|
|
160
|
+
return at === '' ? '(kök)' : at;
|
|
161
|
+
}
|
|
162
|
+
function pointerToken(key) {
|
|
163
|
+
return String(key).replace(/~/g, '~0').replace(/\//g, '~1');
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Anahtarlarından biri büyük/küçük harf duyarsız `$file` olan her nesne ekleme NİYETİDİR. Yalnız tam
|
|
167
|
+
* `{ "$file": "<yol>" }` biçimi eklemedir; dizi/sayı değer, ek anahtar ya da `$File` yazımı sessizce nesne olarak
|
|
168
|
+
* platforma gitmesin diye `malformed`'a düşer (g-22 incelemesi 2).
|
|
169
|
+
*/
|
|
170
|
+
function findInclusions(value, at, found, malformed) {
|
|
171
|
+
if (Array.isArray(value)) {
|
|
172
|
+
value.forEach((item, index) => findInclusions(item, `${at}/${index}`, found, malformed));
|
|
173
|
+
return;
|
|
98
174
|
}
|
|
99
|
-
|
|
100
|
-
|
|
175
|
+
if (!value || typeof value !== 'object')
|
|
176
|
+
return;
|
|
177
|
+
const record = value;
|
|
178
|
+
const keys = Object.keys(record);
|
|
179
|
+
const marker = keys.find(key => key.toLowerCase() === '$file');
|
|
180
|
+
if (marker !== undefined) {
|
|
181
|
+
if (keys.length === 1 && marker === '$file' && typeof record.$file === 'string') {
|
|
182
|
+
found.push({ at, file: record.$file });
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
const problem = marker !== '$file'
|
|
186
|
+
? `anahtar '${marker}' yazılmış, '$file' olmalı`
|
|
187
|
+
: keys.length > 1
|
|
188
|
+
? `'$file' yanında başka anahtar var (${keys.filter(key => key !== '$file').join(', ')})`
|
|
189
|
+
: `'$file' değeri dizgi değil (${JSON.stringify(record[marker])})`;
|
|
190
|
+
malformed.push(`${pointerLabel(at)}: ${problem}`);
|
|
191
|
+
}
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
for (const [key, item] of Object.entries(record))
|
|
195
|
+
findInclusions(item, `${at}/${pointerToken(key)}`, found, malformed);
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Satır içi gövdede `$file` açılmaz: dosya okuma yalnız bodyFile yolunda vardır. Açılmayan nesne platforma nesne
|
|
199
|
+
* olarak giderdi; istek gitmeden söylenir. `$file` anahtarı yoksa yalnız bir ağaç yürüyüşüdür, okuma yapılmaz.
|
|
200
|
+
*/
|
|
201
|
+
function assertNoInlineInclusions(tool, names, args) {
|
|
202
|
+
for (const name of names) {
|
|
203
|
+
const found = [];
|
|
204
|
+
const malformed = [];
|
|
205
|
+
findInclusions(args[name], '', found, malformed);
|
|
206
|
+
const sites = [...found.map(site => pointerLabel(site.at)), ...malformed];
|
|
207
|
+
if (sites.length > 0) {
|
|
208
|
+
throw new PlatformApiError(`${tool}: '${name}' içinde $file nesnesi var (${sites.join(' · ')}) — $file yalnız bodyFile gövdesinde açılır. Metni satır içinde doğrudan yazın ya da gövdeyi bodyFile ile (.json dosyası) verin. İstek gönderilmedi.`, 400);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function replaceInclusions(value, at, texts) {
|
|
213
|
+
const text = texts.get(at);
|
|
214
|
+
if (text !== undefined)
|
|
215
|
+
return text;
|
|
216
|
+
if (Array.isArray(value))
|
|
217
|
+
return value.map((item, index) => replaceInclusions(item, `${at}/${index}`, texts));
|
|
218
|
+
if (value && typeof value === 'object') {
|
|
219
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, replaceInclusions(item, `${at}/${pointerToken(key)}`, texts)]));
|
|
220
|
+
}
|
|
221
|
+
return value;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Eklenen metin bir kez okunur ve olduğu gibi konur: satır sonu korunur, metin yeniden JSON olarak ayrıştırılmaz —
|
|
225
|
+
* eklenen dosyanın içindeki `$file` bu yüzden metin kalır (iç içe ekleme yok). UTF-8 olmayan dosya sessizce � ile
|
|
226
|
+
* bozulacağına reddedilir; baştaki UTF-8 BOM'u çözücü düşürür (`bytes` onu yine sayar). Yer bilgisi (`at`) gövde argümanına göre JSON Pointer'dır.
|
|
227
|
+
*/
|
|
228
|
+
async function includeFiles(tool, value, folder, bodyBytes, roots, config) {
|
|
229
|
+
const sites = [];
|
|
230
|
+
const malformed = [];
|
|
231
|
+
findInclusions(value, '', sites, malformed);
|
|
232
|
+
if (malformed.length > 0) {
|
|
233
|
+
throw new PlatformApiError(`${tool}: bodyFile'da biçimi bozuk $file nesnesi — ${malformed.join(' · ')}. Ekleme yalnız tam { "$file": "<yol>" } biçimidir (tek anahtar, dizgi değer). İstek gönderilmedi.`, 400);
|
|
234
|
+
}
|
|
235
|
+
if (sites.length === 0)
|
|
236
|
+
return { value, files: [] };
|
|
237
|
+
if (sites.length > maxInclusions) {
|
|
238
|
+
throw new PlatformApiError(`${tool}: bodyFile ${sites.length} $file eklemesi taşıyor; sınır ${maxInclusions}. İlk fazlası: ${pointerLabel(sites[maxInclusions]?.at ?? '')} — gövdeyi birden çok çağrıya bölün. İstek gönderilmedi.`, 400);
|
|
239
|
+
}
|
|
240
|
+
const decoder = new TextDecoder('utf-8', { fatal: true });
|
|
241
|
+
const texts = new Map();
|
|
242
|
+
const files = [];
|
|
243
|
+
let total = bodyBytes;
|
|
244
|
+
for (const site of sites) {
|
|
245
|
+
const label = `$file (${pointerLabel(site.at)} → '${site.file}')`;
|
|
246
|
+
const read = await readGuardedFile(tool, label, resolve(folder, site.file), includedExtensions, maxIncludedFileBytes, roots, config);
|
|
247
|
+
total += read.buffer.length;
|
|
248
|
+
if (total > maxBodyFileBytes) {
|
|
249
|
+
throw new PlatformApiError(`${tool}: bodyFile ve $file eklemeleri birlikte 5 MB toplam gövde sınırını aşıyor (${pointerLabel(site.at)} eklemesinde). Tanımı parçalara bölün. İstek gönderilmedi.`, 400);
|
|
250
|
+
}
|
|
251
|
+
try {
|
|
252
|
+
texts.set(site.at, decoder.decode(read.buffer));
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
throw new PlatformApiError(`${tool}: ${label} UTF-8 değil — ${read.real}. Dosyayı UTF-8 olarak kaydedin. İstek gönderilmedi.`, 400);
|
|
256
|
+
}
|
|
257
|
+
files.push({ at: site.at, path: read.real, bytes: read.buffer.length });
|
|
101
258
|
}
|
|
102
|
-
return {
|
|
259
|
+
return { value: replaceInclusions(value, '', texts), files };
|
|
103
260
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { INTERNAL_ROLE_SEPARATOR } from '../../knowledge/componentCatalog.js';
|
|
2
|
+
import { AWAITING_WINDOW_SELECTOR } from './readiness.js';
|
|
2
3
|
import { studioWebTarget } from './targets/studio-web.js';
|
|
3
4
|
/**
|
|
4
5
|
* `swipe` jestinin adım sayısı ve aralığı: react-native-web yanıtlayıcısı tek seferde atılan hareketi jest saymaz
|
|
@@ -38,12 +39,23 @@ export function actionScript(action, captureTarget = studioWebTarget) {
|
|
|
38
39
|
return `(function () {
|
|
39
40
|
var count = performance.getEntriesByType('resource').length;
|
|
40
41
|
var node = document.querySelector(${selector});
|
|
41
|
-
if (!node)
|
|
42
|
+
if (!node) {
|
|
43
|
+
// g-11: açık önizleme penceresi (onay/uyarı/biyometri) sayfanın düğmesini örtüyor olabilir; not hangisi açık söyler.
|
|
44
|
+
var open = document.querySelector('${AWAITING_WINDOW_SELECTOR}');
|
|
45
|
+
return { found: false, applied: false, requests: count, openWindow: open ? { id: String(open.getAttribute('data-element-id')), kind: open.getAttribute('data-dialog-kind') } : null };
|
|
46
|
+
}
|
|
42
47
|
// g-04 sırası: düğümün kendisi basılabilirse o → <id>__press → içteki ilk basılabilir öğe.
|
|
43
48
|
// Mobil çoklu seçim tetikleyicisi kökte role="combobox" taşır ve içindeki ilk role="button"
|
|
44
49
|
// çipin "kaldır" düğmesidir — eskiden ona basılıp seçim siliniyordu (37 S-18). Tek basışlı
|
|
45
50
|
// denetimin yüzeyi <id>__press'tir (ui-native press-role-guard), anahtarınki role="switch".
|
|
46
51
|
var press = document.querySelector(${JSON.stringify(`[data-element-id="${String(action.target ?? '')}${INTERNAL_ROLE_SEPARATOR}press"]`)});
|
|
52
|
+
// g-11 incelemesi: önizleme penceresi (onay/uyarı/biyometri) açıkken altında kalan sayfa öğesine gerçek kullanıcı
|
|
53
|
+
// basamaz; DOM tıklaması basardı ve sayfa eylemi pencere yanıtlanmadan koşardı.
|
|
54
|
+
var shield = document.querySelector('${AWAITING_WINDOW_SELECTOR}');
|
|
55
|
+
if (shield && !shield.contains(node)) {
|
|
56
|
+
var shieldId = String(shield.getAttribute('data-element-id'));
|
|
57
|
+
return { found: true, applied: false, requests: count, openWindow: { id: shieldId, kind: shield.getAttribute('data-dialog-kind') }, note: "önizleme penceresi " + shieldId + " açık ve sayfayı örtüyor: gerçek kullanıcı alttaki öğeye basamaz, eylem uygulanmadı — önce pencereyi yanıtlayın (" + shieldId + "__confirm / __cancel, biyometride __verify / __cancel)" };
|
|
58
|
+
}
|
|
47
59
|
var target = ${pressSurface} || node.matches('button, a, [role="button"], [role="switch"], [role="combobox"]') ? node
|
|
48
60
|
: (press || node.querySelector('button, a, [role="button"], [role="switch"]') || node);
|
|
49
61
|
// S-2: sekme grubunun KÖKÜNE click, DOM sırasındaki ilk sekmeye gider ve "uygulandı" der —
|
|
@@ -433,6 +445,13 @@ export function actionScript(action, captureTarget = studioWebTarget) {
|
|
|
433
445
|
// K1 (g-04): gerçek kullanıcı alana odaklanır, yazar ve çıkar. Blur'suz yazımda odak kaybına
|
|
434
446
|
// bağlı akış (ui-native girişlerin 'changed'i) hiç koşmuyordu (37 S-19). focus() odağı alamazsa
|
|
435
447
|
// (görünmez alan, odaksız belge) olaylar elle atılır: Svelte 'blur'u, React 'focusout'u dinler.
|
|
448
|
+
// blur: false (g-12) telefonda klavye açıkken yazılan hâli sınar: odak alanda kalır, not söyler.
|
|
449
|
+
var keepFocus = ${JSON.stringify(action.blur === false)};
|
|
450
|
+
var done = function () {
|
|
451
|
+
var result = { found: true, applied: true, requests: count };
|
|
452
|
+
if (keepFocus) result.note = "blur: false — odak alanda kaldı; odak kaybına bağlı akış ('changed') bu adımda koşmaz";
|
|
453
|
+
return finish(result);
|
|
454
|
+
};
|
|
436
455
|
var focusField = function () {
|
|
437
456
|
field.focus();
|
|
438
457
|
if (document.activeElement === field) return;
|
|
@@ -448,15 +467,15 @@ export function actionScript(action, captureTarget = studioWebTarget) {
|
|
|
448
467
|
if (field instanceof HTMLInputElement && (field.type === 'checkbox' || field.type === 'radio')) {
|
|
449
468
|
var shouldCheck = wanted === 'true' || wanted === '1' || wanted === 'on';
|
|
450
469
|
if (field.checked !== shouldCheck) field.click();
|
|
451
|
-
blurField();
|
|
452
|
-
return
|
|
470
|
+
if (!keepFocus) blurField();
|
|
471
|
+
return done();
|
|
453
472
|
}
|
|
454
473
|
var setter = Object.getOwnPropertyDescriptor(proto, 'value').set;
|
|
455
474
|
setter.call(field, ${value});
|
|
456
475
|
field.dispatchEvent(new Event('input', { bubbles: true }));
|
|
457
476
|
field.dispatchEvent(new Event('change', { bubbles: true }));
|
|
458
|
-
blurField();
|
|
459
|
-
return
|
|
477
|
+
if (!keepFocus) blurField();
|
|
478
|
+
return done();
|
|
460
479
|
})()`;
|
|
461
480
|
}
|
|
462
481
|
/** `rf_range__start` → `rf_range`: iç rol hedefinin prop'ları kök düğümde yaşar. */
|
|
@@ -6,7 +6,7 @@ import { PersistentScreenshotBrowser, persistentContextOpener, screenshotProfile
|
|
|
6
6
|
import { actionScript, activeTabScript, rootElementId, runtimePropsScript } from './actions.js';
|
|
7
7
|
import { domSummaryScript } from './summary.js';
|
|
8
8
|
import { createPageConsoleLog, describePageFailure, isExpressionEcho, parseRenderErrors, renderErrorsScript, splitExpressionErrors } from './console-log.js';
|
|
9
|
-
import { decideReadiness, describeUnready, parseReadiness, readinessScript, runtimePath } from './readiness.js';
|
|
9
|
+
import { awaitingUserNote, decideReadiness, describeUnready, parseAwaitingWindow, parseReadiness, readinessScript, runtimePath } from './readiness.js';
|
|
10
10
|
/** Runtime'ın Connect Data bağlantısı için bastığı sözde akış kimliği: hata bir akış adımından değil bağlı prop'un okumasından gelir. */
|
|
11
11
|
const CONNECT_DATA_FLOW_ID = 'connect-data';
|
|
12
12
|
/**
|
|
@@ -159,6 +159,9 @@ const resizeSettleBudgetMs = 3000;
|
|
|
159
159
|
* çağrıya ve ~3.5 dakikaya mal oldu). İkiden fazlası turu uzatır, bir şey kazandırmaz.
|
|
160
160
|
*/
|
|
161
161
|
const navigationAttempts = 2;
|
|
162
|
+
/** g-12 oturumları: depo durumu çerez (jeton dahil) taşıdığı için diske değil süreç belleğine yazılır. */
|
|
163
|
+
const sessionTtlMs = 30 * 60_000;
|
|
164
|
+
const maxSessions = 8;
|
|
162
165
|
/**
|
|
163
166
|
* Yalnız zaman aşımı yeniden denenir. `net::ERR_CONNECTION_REFUSED` / DNS gibi hızlı düşen
|
|
164
167
|
* hatalarda Studio ayakta değildir; tekrar yalnız bekleme süresini iki katına çıkarır ve
|
|
@@ -232,6 +235,7 @@ export class StudioScreenshotter {
|
|
|
232
235
|
queue = Promise.resolve();
|
|
233
236
|
pending = 0;
|
|
234
237
|
recycleNext = false;
|
|
238
|
+
sessions = new Map();
|
|
235
239
|
constructor(config, launcher) {
|
|
236
240
|
this.config = config;
|
|
237
241
|
this.launcher = launcher;
|
|
@@ -307,7 +311,7 @@ export class StudioScreenshotter {
|
|
|
307
311
|
* işaretlenir ve kare yine çekilir, çünkü bozuk/boş sayfayı görmek de bir doğrulamadır. Veri
|
|
308
312
|
* yanıtından sonra fontlar + kısa yerleşme beklemesi Svelte hydrate'ine alan bırakır.
|
|
309
313
|
*/
|
|
310
|
-
async runActions(page, request, consoleLog) {
|
|
314
|
+
async runActions(page, context, request, consoleLog) {
|
|
311
315
|
if (!request.actions || request.actions.length === 0)
|
|
312
316
|
return undefined;
|
|
313
317
|
const outcomes = [];
|
|
@@ -319,12 +323,23 @@ export class StudioScreenshotter {
|
|
|
319
323
|
}
|
|
320
324
|
const snapshotBefore = parseReadiness(await page.evaluate(readinessScript).catch(() => undefined));
|
|
321
325
|
const activityBefore = snapshotBefore?.activity ?? null;
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
326
|
+
// `network` (g-12) sayfada betik koşturmaz: bağlamın ağını değiştirir ve sayfanın gördüğü `navigator.onLine`'ı
|
|
327
|
+
// okur. Ağ açılınca koşan eşitleme akışları aşağıdaki aynı hazırlık beklemesiyle `flowRuns`'a yazılır.
|
|
328
|
+
const raw = action.action === 'network'
|
|
329
|
+
? context.setOffline === undefined
|
|
330
|
+
? { found: true, applied: false, note: 'ağ denetimi yok: tarayıcı bağlamı setOffline desteklemiyor — ağ DEĞİŞMEDİ', requests: 0 }
|
|
331
|
+
: await (async () => {
|
|
332
|
+
const count = await page.evaluate("performance.getEntriesByType('resource').length").catch(() => 0);
|
|
333
|
+
await context.setOffline?.(action.value === 'offline');
|
|
334
|
+
const online = await page.evaluate('navigator.onLine').catch(() => undefined);
|
|
335
|
+
return { found: true, applied: true, requests: typeof count === 'number' ? count : 0, ...(typeof online === 'boolean' ? { online } : {}) };
|
|
336
|
+
})()
|
|
337
|
+
: await page.evaluate(actionScript(action, request.target)).catch(error => ({
|
|
338
|
+
found: false,
|
|
339
|
+
applied: false,
|
|
340
|
+
note: error instanceof Error ? error.message : String(error),
|
|
341
|
+
requests: 0
|
|
342
|
+
}));
|
|
328
343
|
const before = (raw ?? {});
|
|
329
344
|
// Hedef bulunamadıysa ajan neyi yanlış yazdığını KAREDEN çıkaramaz. `__` taşıyan hedef bir
|
|
330
345
|
// iç rol denemesidir: bilinen roller sayılır. Kök hedefi bulunamadıysa sorun elementId'nin
|
|
@@ -335,6 +350,9 @@ export class StudioScreenshotter {
|
|
|
335
350
|
: action.target.includes(INTERNAL_ROLE_SEPARATOR)
|
|
336
351
|
? `'${action.target}' sayfada yok. Bu bir İÇ ROL hedefi: biçim <elementId>__<rol> ve bilinen roller — ${knownInternalRoles().join(' · ')}. Roller görünen metinden değil kayıttaki anahtardan türer (sekme key'i, kart id'si); hedef bileşenin rollerini component_get çıktısındaki internalRoles verir.`
|
|
337
352
|
: `'${action.target}' elementId'si sayfada yok. page_schema_get ile elementId'leri listeleyin; bir komponentin İÇ kontrolünü hedefliyorsanız <elementId>__<rol> biçimini kullanın (component_get çıktısındaki internalRoles).`;
|
|
353
|
+
const openWindow = parseAwaitingWindow(before.openWindow);
|
|
354
|
+
if (openWindow !== null)
|
|
355
|
+
before.note = `${before.note} Açık önizleme penceresi: ${openWindow.id}${openWindow.kind === null ? '' : ` (${openWindow.kind})`}; sayfanın düğmeleri onun altında kalır.`;
|
|
338
356
|
}
|
|
339
357
|
// Olay akışı uç çağırıyor: yerleşme beklemeden sonraki istek sayısını ölçmek "istek gitti mi"
|
|
340
358
|
// sorusunu cevaplar — koşuda kusuru kesinleştiren gözlem buydu.
|
|
@@ -376,9 +394,11 @@ export class StudioScreenshotter {
|
|
|
376
394
|
: undefined;
|
|
377
395
|
const propWritten = typeof propsAfter === 'string' ? propsAfter !== before.propsBefore : undefined;
|
|
378
396
|
const applied = before.applied === true && propWritten !== false;
|
|
397
|
+
// g-11: eylem bir önizleme penceresi açtıysa (onay, uyarı, biyometri) sıradaki eylemin neye basacağı burada söylenir.
|
|
398
|
+
const awaitingNote = readiness.state === 'awaiting-user' ? awaitingUserNote(readiness.snapshot?.awaitingWindow) : undefined;
|
|
379
399
|
const note = propWritten === false
|
|
380
400
|
? `değer DOM'a yazıldı ama '${rootElementId(String(action.target ?? ''))}' düğümünün runtime prop'ları DEĞİŞMEDİ — bileşen olayı kendi change yoluna çevirmedi ya da değer zaten aynıydı. Çok alanlı bileşende <elementId>__<rol> kutusunu hedefleyin (component_get → internalRoles).`
|
|
381
|
-
: before.note;
|
|
401
|
+
: [before.note, awaitingNote].filter((part) => part !== undefined && part !== '').join(' ') || undefined;
|
|
382
402
|
outcomes.push({
|
|
383
403
|
index,
|
|
384
404
|
action: String(action.action),
|
|
@@ -395,6 +415,7 @@ export class StudioScreenshotter {
|
|
|
395
415
|
...(before.optionsSeen === undefined ? {} : { optionsSeen: before.optionsSeen }),
|
|
396
416
|
...(before.waitedMs === undefined ? {} : { waitedMs: before.waitedMs }),
|
|
397
417
|
...(before.submitVia === undefined ? {} : { submitVia: before.submitVia }),
|
|
418
|
+
...(before.online === undefined ? {} : { online: before.online }),
|
|
398
419
|
pageState: readiness.state
|
|
399
420
|
});
|
|
400
421
|
}
|
|
@@ -491,7 +512,47 @@ export class StudioScreenshotter {
|
|
|
491
512
|
context = await browser.newContext(contextOptions);
|
|
492
513
|
}
|
|
493
514
|
logStep('tarayıcı →', `GET ${request.url} · viewport ${request.viewport.width}x${request.viewport.height}${request.fullPage ? ' · fullPage' : ''}${request.token ? '' : ' · gateway jetonu YOK'}${(request.cookies ?? []).length > 0 ? ` · caller çerezleri: ${(request.cookies ?? []).map(cookie => cookie.name).join(', ')}` : ''}${request.token || (request.cookies ?? []).length > 0 ? '' : ' · ANONİM (çerez YOK)'}${request.colorScheme ? ` · tema ${request.colorScheme}` : ''}${request.motion === 'play' ? ' · hareket oynar' : ''}${request.headers ? ` · başlık: ${Object.keys(request.headers).join(', ')}` : ''}`);
|
|
515
|
+
let sessionRestored;
|
|
516
|
+
// Oturum depoyu bir kez kaydeder: başarılı yolda yanıttan önce, hata yolunda `finally`'de. Yükleme yarıda kaldıysa
|
|
517
|
+
// (`setStorageState` önce siler, sonra yazar) yarı boş depo iyi kaydın üstüne yazılmaz (g-12 inceleme #6).
|
|
518
|
+
let sessionPending = request.session !== undefined;
|
|
519
|
+
// Okunamayan depo eski kaydı BIRAKMAZ: bırakılsaydı sonraki çekim aradaki yazmaları sessizce kaybeden eski depoyu
|
|
520
|
+
// `sessionRestored: true` ile yüklerdi (g-12 inceleme #5).
|
|
521
|
+
const saveSession = async () => {
|
|
522
|
+
sessionPending = false;
|
|
523
|
+
if (request.session === undefined || !context.storageState)
|
|
524
|
+
return true;
|
|
525
|
+
const state = await context.storageState().catch(() => undefined);
|
|
526
|
+
this.sessions.delete(request.session);
|
|
527
|
+
if (state === undefined)
|
|
528
|
+
return false;
|
|
529
|
+
this.sessions.set(request.session, { state, at: Date.now() });
|
|
530
|
+
for (const [key, entry] of this.sessions) {
|
|
531
|
+
if (Date.now() - entry.at > sessionTtlMs || this.sessions.size > maxSessions)
|
|
532
|
+
this.sessions.delete(key);
|
|
533
|
+
}
|
|
534
|
+
return true;
|
|
535
|
+
};
|
|
494
536
|
try {
|
|
537
|
+
// Oturum çerezlerden ÖNCE yüklenir: `setStorageState` mevcut depoyu siler, sonra yazılan jeton çerezi kalsın.
|
|
538
|
+
// Yeteneği olmayan bağlamda çağrı sessizce taze depoyla çekilmez (colorScheme'in kuralı).
|
|
539
|
+
if (request.session !== undefined) {
|
|
540
|
+
if (!context.storageState || !context.setStorageState) {
|
|
541
|
+
throw new PlatformApiError(`session='${request.session.split('|')[0]}' uygulanamadı: tarayıcı bağlamı depo durumunu okuyup yükleyemiyor (storageState/setStorageState). Kare taze depoyla çekilirdi ve bu sessizce yanlış olurdu.`);
|
|
542
|
+
}
|
|
543
|
+
const saved = this.sessions.get(request.session);
|
|
544
|
+
if (saved !== undefined && Date.now() - saved.at > sessionTtlMs)
|
|
545
|
+
this.sessions.delete(request.session);
|
|
546
|
+
sessionRestored = saved !== undefined && Date.now() - saved.at <= sessionTtlMs;
|
|
547
|
+
// İlk çağrı da boş depoyla başlar: kalıcı profilde önceki çekimlerden kalan köken anlık görüntüye girmesin (#8).
|
|
548
|
+
try {
|
|
549
|
+
await context.setStorageState(sessionRestored && saved !== undefined ? saved.state : { cookies: [], origins: [] });
|
|
550
|
+
}
|
|
551
|
+
catch (error) {
|
|
552
|
+
sessionPending = false;
|
|
553
|
+
throw error;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
495
556
|
const cookies = new Map();
|
|
496
557
|
if (request.token)
|
|
497
558
|
cookies.set('NG_AccessToken', request.token);
|
|
@@ -573,7 +634,7 @@ export class StudioScreenshotter {
|
|
|
573
634
|
: readiness.state === 'not-ready' ? false : undefined;
|
|
574
635
|
const finalHref = readiness.snapshot?.href;
|
|
575
636
|
const finalUrl = finalHref !== undefined && runtimePath(finalHref) !== runtimePath(request.url) ? finalHref : undefined;
|
|
576
|
-
const actions = await this.runActions(page, request, consoleLog);
|
|
637
|
+
const actions = await this.runActions(page, context, request, consoleLog);
|
|
577
638
|
// Ölçüm EYLEMLERDEN SONRA: eylem sekme değiştirir, filtre uygular, panel açar — içerik
|
|
578
639
|
// boyu o zaman kesinleşir. Eskiden ölçüm eylemlerden önceydi ve ikinci sekmede 40 satırlık
|
|
579
640
|
// tablo açan bir çekim viewport boyunda kalıyordu; `contentHeight` ilk sekmenin boyuydu
|
|
@@ -643,6 +704,7 @@ export class StudioScreenshotter {
|
|
|
643
704
|
if (runtimeErrors.length > 0) {
|
|
644
705
|
logStep('akış hatası ✕', `${runtimeErrors.length} akış adımı hata bıraktı — sayfa yine 200 döndü`, runtimeErrors.map(error => `${error.step?.name ?? '?'}${error.element ? ` → ${error.element}.${error.property ?? '?'}` : ''}: ${error.message ?? ''}`));
|
|
645
706
|
}
|
|
707
|
+
const sessionSaved = await saveSession();
|
|
646
708
|
return {
|
|
647
709
|
...(png === undefined ? {} : { png, mimeType: format === 'jpeg' ? 'image/jpeg' : 'image/png' }),
|
|
648
710
|
...(png === undefined ? {} : (size => size === undefined ? {} : { imageSize: size })(imageSize(png))),
|
|
@@ -653,6 +715,7 @@ export class StudioScreenshotter {
|
|
|
653
715
|
renderWaitTimedOut: apiStatus === undefined,
|
|
654
716
|
elapsedMs,
|
|
655
717
|
pageState: readiness.state,
|
|
718
|
+
...(readiness.state === 'awaiting-user' && readiness.snapshot?.awaitingWindow ? { awaitingWindow: readiness.snapshot.awaitingWindow } : {}),
|
|
656
719
|
readyAfterMs: readiness.waitedMs,
|
|
657
720
|
...(finalUrl === undefined ? {} : { finalUrl }),
|
|
658
721
|
...(measured === undefined ? {} : { contentHeight: measured }),
|
|
@@ -665,10 +728,16 @@ export class StudioScreenshotter {
|
|
|
665
728
|
...(pageErrors.length > 0 ? { pageErrors } : {}),
|
|
666
729
|
...(renderErrors.length > 0 ? { renderErrors } : {}),
|
|
667
730
|
...(flowsSettled === undefined ? {} : { flowsSettled }),
|
|
668
|
-
...(navigationRetried ? { navigationRetried: true } : {})
|
|
731
|
+
...(navigationRetried ? { navigationRetried: true } : {}),
|
|
732
|
+
...(sessionRestored === undefined ? {} : { sessionRestored }),
|
|
733
|
+
...(sessionSaved ? {} : { sessionSaved: false })
|
|
669
734
|
};
|
|
670
735
|
}
|
|
671
736
|
finally {
|
|
737
|
+
// Hata yolunda da depo kapanıştan ÖNCE alınır (kalıcı profil kapanışta depoyu siler): ağsız yazılmış kayıt
|
|
738
|
+
// çekim düşse de oturumda kalır. En yenisi sona yazılır; süresi dolan ve sekizi aşan en eski oturum düşer.
|
|
739
|
+
if (sessionPending)
|
|
740
|
+
await saveSession();
|
|
672
741
|
await context.close().catch(() => undefined);
|
|
673
742
|
// Zamanlayıcı ÖNCE temizlenir: eskiden her çekim yenisini kurup eskisini bırakıyordu ve
|
|
674
743
|
// kaçan zamanlayıcı sonraki çekimin ortasında tarayıcıyı kapatıyordu (S-3).
|
|
@@ -72,6 +72,8 @@ export class PersistentScreenshotBrowser {
|
|
|
72
72
|
* Tek çekimlik oturum. Eski taze bağlamın yalıtımı korunur: başlık ve init script'i bağlama
|
|
73
73
|
* değil sayfaya yazılır (sonraki çekime sızmaz — `impersonated-user-id` dahil), kapanışta
|
|
74
74
|
* çerezler ve gezilen kökenlerin depoları (localStorage, IndexedDB, …) diskteki profilden silinir.
|
|
75
|
+
* `session` (g-12) bu silmeyi atlamaz: depo kapanıştan önce okunur, sonraki çekimde yüklenir; yüklenen kökenler de
|
|
76
|
+
* kapanışta silinir. Ağ kesildiyse sayfalar kapandıktan SONRA geri açılır — bağlam sonraki çekimle paylaşılır.
|
|
75
77
|
*/
|
|
76
78
|
class PersistentContextSession {
|
|
77
79
|
context;
|
|
@@ -79,6 +81,8 @@ class PersistentContextSession {
|
|
|
79
81
|
initScripts = [];
|
|
80
82
|
headers;
|
|
81
83
|
pages = [];
|
|
84
|
+
restoredOrigins = new Set();
|
|
85
|
+
offline = false;
|
|
82
86
|
constructor(context, options) {
|
|
83
87
|
this.context = context;
|
|
84
88
|
this.options = options;
|
|
@@ -92,6 +96,20 @@ class PersistentContextSession {
|
|
|
92
96
|
async setExtraHTTPHeaders(headers) {
|
|
93
97
|
this.headers = headers;
|
|
94
98
|
}
|
|
99
|
+
async setOffline(offline) {
|
|
100
|
+
await this.context.setOffline(offline);
|
|
101
|
+
this.offline = offline;
|
|
102
|
+
}
|
|
103
|
+
storageState() {
|
|
104
|
+
return this.context.storageState({ indexedDB: true });
|
|
105
|
+
}
|
|
106
|
+
async setStorageState(state) {
|
|
107
|
+
for (const entry of state.origins ?? []) {
|
|
108
|
+
if (typeof entry.origin === 'string')
|
|
109
|
+
this.restoredOrigins.add(entry.origin);
|
|
110
|
+
}
|
|
111
|
+
await this.context.setStorageState(state);
|
|
112
|
+
}
|
|
95
113
|
async newPage() {
|
|
96
114
|
const page = await this.context.newPage();
|
|
97
115
|
this.pages.push(page);
|
|
@@ -104,7 +122,7 @@ class PersistentContextSession {
|
|
|
104
122
|
return page;
|
|
105
123
|
}
|
|
106
124
|
async close() {
|
|
107
|
-
const origins = new Set();
|
|
125
|
+
const origins = new Set(this.restoredOrigins);
|
|
108
126
|
for (const page of this.pages) {
|
|
109
127
|
try {
|
|
110
128
|
const origin = new URL(page.url()).origin;
|
|
@@ -126,5 +144,9 @@ class PersistentContextSession {
|
|
|
126
144
|
await this.context.clearCookies().catch(() => undefined);
|
|
127
145
|
for (const page of this.pages)
|
|
128
146
|
await page.close().catch(() => undefined);
|
|
147
|
+
// Ağ EN SON açılır: sayfa açıkken açılsaydı çevrimdışı uygulama `online` olayıyla kuyruğunu sunucuya boşaltırdı —
|
|
148
|
+
// salt okunur bir çekimden görünmez yazma, oturumla taşınan kuyrukta da çift kayıt (g-12 inceleme #4).
|
|
149
|
+
if (this.offline)
|
|
150
|
+
await this.context.setOffline(false).catch(() => undefined);
|
|
129
151
|
}
|
|
130
152
|
}
|