@appaflytech/zeyrek-ai-mcp 0.0.4 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +8 -3
- package/.env.production.example +2 -0
- package/README.md +229 -193
- package/dist/config.js +20 -2
- package/dist/knowledge/backendConstraints.js +897 -0
- package/dist/knowledge/componentCatalog.js +196 -42
- package/dist/knowledge/coverage.js +43 -0
- package/dist/knowledge/flowGuards.js +30 -0
- package/dist/knowledge/guideSections.js +49 -0
- package/dist/knowledge/hydrate.js +97 -0
- package/dist/knowledge/languageCodes.js +36 -0
- package/dist/knowledge/languages.js +105 -0
- package/dist/knowledge/layoutSummary.js +10 -0
- package/dist/knowledge/localization.js +51 -0
- package/dist/knowledge/normalize.js +449 -16
- package/dist/knowledge/outlines.js +136 -12
- package/dist/knowledge/propTypes.js +80 -0
- package/dist/knowledge/rules.js +618 -43
- package/dist/knowledge/schemaGuard.js +199 -0
- package/dist/knowledge/styleResolve.js +63 -4
- package/dist/observability/trace.js +27 -3
- package/dist/platform/errors.js +46 -4
- package/dist/platform/http.js +33 -4
- package/dist/platform/patch.js +27 -0
- package/dist/platform/refs.js +104 -0
- package/dist/platform/responses.js +9 -2
- package/dist/platform/screenshot.js +527 -16
- package/dist/platform/secrets.js +40 -0
- package/dist/platform/tokenVault.js +89 -0
- package/dist/platform/writeDiff.js +281 -0
- package/dist/resources/examples/layout-app-shell.json +1 -1
- package/dist/resources/examples/microservice-table-list.json +2 -4
- package/dist/resources/examples/page-dynamic-form.json +2 -2
- package/dist/resources/examples/page-flex-sections.json +99 -0
- package/dist/resources/examples/page-sign-in.json +3 -2
- package/dist/resources/guides/component.md +69 -8
- package/dist/resources/guides/entity.md +404 -16
- package/dist/resources/guides/expressions.md +44 -2
- package/dist/resources/guides/external-component.md +9 -0
- package/dist/resources/guides/getting-started.md +77 -7
- package/dist/resources/guides/layout.md +53 -10
- package/dist/resources/guides/microservice.md +226 -13
- package/dist/resources/guides/middleware.md +89 -1
- package/dist/resources/guides/page.md +383 -46
- package/dist/resources/guides/translation.md +126 -8
- package/dist/resources/guides/view.md +16 -1
- package/dist/resources/guides/workflow.md +198 -27
- package/dist/resources/knowledge.js +5 -0
- package/dist/resources/rules.js +27 -0
- package/dist/resources/schema-fingerprint.json +8 -0
- package/dist/resources/schemas/CreateBulkRowCommand.json +33 -0
- package/dist/resources/schemas/CreateDatasourceCommand.json +15 -6
- package/dist/resources/schemas/CreateDiscoveredEntitiesCommand.json +19 -8
- package/dist/resources/schemas/CreateEntityCommand.json +52 -12
- package/dist/resources/schemas/CreateMicroserviceCommand.json +26 -2
- package/dist/resources/schemas/CreateQueryCommand.json +56 -3
- package/dist/resources/schemas/CreateRowCommand.json +38 -0
- package/dist/resources/schemas/CreateViewCommand.json +13 -10
- package/dist/resources/schemas/CreateWorkflowCommand.json +95 -14
- package/dist/resources/schemas/GetRowsQuery.json +3 -0
- package/dist/resources/schemas/PreviewMicroserviceCommand.json +16 -0
- package/dist/resources/schemas/PreviewQueryCommand.json +5 -2
- package/dist/resources/schemas/PreviewWorkflowCommand.json +34 -9
- package/dist/resources/schemas/StepDto.json +23 -3
- package/dist/resources/schemas/UpdateBulkRowCommand.json +33 -0
- package/dist/resources/schemas/UpdateDatasourceCommand.json +15 -6
- package/dist/resources/schemas/UpdateEntityCommand.json +49 -10
- package/dist/resources/schemas/UpdateMicroserviceCommand.json +28 -3
- package/dist/resources/schemas/UpdateQueryCommand.json +13 -3
- package/dist/resources/schemas/UpdateRowCommand.json +40 -0
- package/dist/resources/schemas/UpdateViewCommand.json +15 -10
- package/dist/resources/schemas/UpdateWorkflowCommand.json +95 -14
- package/dist/server.js +4 -0
- package/dist/tools/access.js +3 -3
- package/dist/tools/applications.js +1 -1
- package/dist/tools/automations.js +63 -30
- package/dist/tools/common.js +39 -0
- package/dist/tools/components.js +16 -7
- package/dist/tools/datasources.js +83 -21
- package/dist/tools/deployments.js +34 -2
- package/dist/tools/dtos.js +25 -12
- package/dist/tools/entities.js +113 -28
- package/dist/tools/externalComponents.js +9 -3
- package/dist/tools/files.js +20 -6
- package/dist/tools/flows.js +110 -11
- package/dist/tools/folders.js +7 -2
- package/dist/tools/globalFunctions.js +63 -0
- package/dist/tools/layouts.js +103 -29
- package/dist/tools/microservices.js +229 -38
- package/dist/tools/organizations.js +5 -6
- package/dist/tools/pages.js +365 -100
- package/dist/tools/platformInfo.js +152 -11
- package/dist/tools/projects.js +183 -33
- package/dist/tools/queries.js +32 -10
- package/dist/tools/rows.js +159 -45
- package/dist/tools/screenWrite.js +59 -0
- package/dist/tools/screens.js +28 -0
- package/dist/tools/translations.js +24 -8
- package/dist/tools/views.js +20 -10
- package/dist/tools/workflows.js +33 -16
- package/package.json +6 -3
package/dist/tools/pages.js
CHANGED
|
@@ -1,15 +1,36 @@
|
|
|
1
|
-
import { guid, projectParam, environmentParam, pagingParams, readOnly, writes, updates, destructive } from './common.js';
|
|
2
|
-
import { orphanedFlowWarnings, normalizeComponentDefaults, normalizeSchemaOps, schemaOpValidationErrors, validationNote, componentDefaultsNote, replacePropNote, ensurePageContainer, retargetRootAdds, rootAddsNote,
|
|
3
|
-
import { fetchHost, pageUpdateBody, schemaParam, eventsParam, unwrap, modeParam, summarize, findNode } from './screens.js';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
1
|
+
import { guid, projectParam, environmentParam, pagingParams, readOnly, writes, updates, destructive, microOps } from './common.js';
|
|
2
|
+
import { orphanedFlowWarnings, orphanedFlowPlan, orphanedFlowMessage, staleSetterWarnings, normalizeComponentDefaults, normalizeSchemaOps, schemaOpValidationErrors, validationNote, componentDefaultsNote, replacePropNote, ensurePageContainer, retargetRootAdds, rootAddsNote, flexSizeWarnings, reorderSchemaBlocks, reorderNote, nestedRepeaterNodes, nestedRepeaterMessage, stringStyleNodes, stringStyleWarning, inertFlexPropNodes, inertFlexPropWarning, isFlexContainer, FLEX_LAYOUT_PROP_NAMES, resetPageContainerPosition, pageContainerPositionWarning, verticalGapWarnings, withNormalization, PAGE_CONTAINER_ELEMENT_ID } from '../knowledge/normalize.js';
|
|
3
|
+
import { fetchHost, pageUpdateBody, schemaParam, eventsParam, unwrap, modeParam, summarize, outlineAppliedPatch, findNode, flowPath } from './screens.js';
|
|
4
|
+
import { fetchComponentEventNames } from './components.js';
|
|
5
|
+
import { assertScreenWriteGuards, screenCatalogNotes } from './screenWrite.js';
|
|
6
|
+
import { formBindingWarnings, formSubmitButtonWarnings } from '../knowledge/coverage.js';
|
|
7
|
+
import { schemaOpDrops, silentlyDropped, unwrapProps, withWriteDiff } from '../platform/writeDiff.js';
|
|
6
8
|
import { PlatformApiError } from '../platform/errors.js';
|
|
7
9
|
import { handle, toolResult, toolError } from '../platform/responses.js';
|
|
8
10
|
import { StudioScreenshotter } from '../platform/screenshot.js';
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
+
import { summarizeLayout } from '../knowledge/layoutSummary.js';
|
|
12
|
+
import { resolveIdentityToken } from '../platform/tokenVault.js';
|
|
13
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
14
|
+
import { dirname, resolve } from 'node:path';
|
|
15
|
+
import { applyPropTypesToSchema, setPropTypeErrors } from '../knowledge/propTypes.js';
|
|
11
16
|
import { z } from 'zod';
|
|
12
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Yazma yanıtındaki şema gövdesinin biçimi (B-55 · S-9). Varsayılan `outline`: gövde ajanın
|
|
19
|
+
* aynı turda kendi gönderdiği içeriğin yankısıdır, kanıt ise pointer'da ve `silentlyDropped`ta
|
|
20
|
+
* durur. Bayrağı bilmeyen ajan pahalı olanı ödemesin diye kırpma OPT-OUT'tur (aynı karar:
|
|
21
|
+
* `DS-004`, `datasource_configuration_options`).
|
|
22
|
+
*/
|
|
23
|
+
const writeViewParam = z.enum(['outline', 'full']).optional().describe("Yanıttaki şema gövdesinin biçimi. outline (VARSAYILAN): komponent ağaçları iskelete iner (id/elementId/name/type/children), appliedPatch her op'un op+path'ini verir — 'yalnız o komponente mi dokunuldu' kanıtı JSON Pointer'dadır, 'kaydedildi mi' kanıtı silentlyDropped'tadır. full: platformun döndürdüğü gövde aynen gelir.");
|
|
24
|
+
/**
|
|
25
|
+
* `appliedPatch` zarfın altında da gelebilir (`{ data: { appliedPatch } }`), düz de.
|
|
26
|
+
*/
|
|
27
|
+
function outlineWriteResponse(response) {
|
|
28
|
+
const inner = response.data;
|
|
29
|
+
const container = (inner && typeof inner === 'object' && !Array.isArray(inner) ? inner : response);
|
|
30
|
+
if (Array.isArray(container.appliedPatch))
|
|
31
|
+
container.appliedPatch = outlineAppliedPatch(container.appliedPatch);
|
|
32
|
+
}
|
|
33
|
+
const opsParam = z.array(z.record(z.unknown())).describe("Micro-op listesi. Op sözlüğü: replace_prop {target, prop, value} | set_binding {target, prop, expression: '{{ Page.variables.x }}'} | add_component {parent (üst elementId ya da 'root'), index?, component} | remove_component {target} | move_component {target, parent, index?, position: {x, y}} | set_event {target, event, eventValue} | remove_event {target, event} (gateway emülasyonu: platformun micro-op'u olay SİLEMEZ, eventValue: null yalnız değersiz ölü kayıt bırakır — bu op şemayı okuyup olayı diziden çıkarır ve sayfayı tam gövdeyle geri yazar) | set_validations {target, validations: [{type, message, value, isActive}]} (komponentin validation listesini TAMAMEN değiştirir; boş dizi hepsini siler) | resize_component {target, size?: {w,h}, appearance?: {widthType,widthString,heightType,heightString}, states?: {isWrapper,isScrollable,isSticky,isDroppable,isFloating,isQuickStack}, breakpoint?: 1024} (gateway emülasyonu: platformun micro-op'unda boyut YOKTUR ve move_component'e konan size sessizce düşer — bu op şemayı okuyup hedefin boyutunu değiştirir ve sayfayı tam gövdeyle geri yazar. Izgarada blocks[].size, flex'te appearance geçerlidir; ebeveynin düzeni yazılanı etkisiz kılıyorsa PAGE-019 uyarısı çıkar. states bir PROP DEĞİL düğüm alanıdır ve replace_prop onu yazamaz — kurulmuş bir düğümün isWrapper bayrağını değiştirmenin tek yolu buydu). set_prop_type {target, prop, propType: 'static'|'code'|'translation', ref?, data?} (gateway emülasyonu: replace_prop gönderilen ham değeri HER ZAMAN static Prop'una sarar ve sarmalanmış Prop nesnesini reddeder — kurulu bir etiketi çeviriye bağlamanın tek yolu düğümü remove_component + add_component ile yeniden kurmaktı ve o yol düğüme yazan setter iteratee'lerini sessizce öldürüyordu. Bu op şemayı okuyup prop'un TİPİNİ yerinde değiştirir ve sayfayı tam gövdeyle geri yazar. Taşıyıcı alan tipe göre: static → data, code ve translation → ref (runtime data'yı her turda ezer, anahtar yalnız ref'te yaşar). Eksik taşıyıcı istek gönderilmeden reddedilir; page_schema_validate aynı kararı verir). target = elementId.");
|
|
13
34
|
/**
|
|
14
35
|
* PAGE-008 — `set_event` uydurma bir olay adıyla 200 döner ve bağ SESSİZCE ölü kalır: motor olayı
|
|
15
36
|
* komponentin yaydığı adla eşleştirir, eşleşme yoksa akış hiç çalışmaz. Kontrol yalnız hedefin
|
|
@@ -37,17 +58,6 @@ function collectTypes(nodes, into) {
|
|
|
37
58
|
collectTypes(node.children, into);
|
|
38
59
|
}
|
|
39
60
|
}
|
|
40
|
-
function invalidComponentNames(nodes) {
|
|
41
|
-
const found = [];
|
|
42
|
-
for (const node of nodes ?? []) {
|
|
43
|
-
const name = node.name;
|
|
44
|
-
if (typeof name === 'string' && name.length > 0 && !/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name.replace(/-/g, ''))) {
|
|
45
|
-
found.push(`'${name}' (${node.elementId ?? '?'})`);
|
|
46
|
-
}
|
|
47
|
-
found.push(...invalidComponentNames(node.children));
|
|
48
|
-
}
|
|
49
|
-
return found;
|
|
50
|
-
}
|
|
51
61
|
function removeNodeFromTree(nodes, target) {
|
|
52
62
|
for (let index = 0; index < nodes.length; index++) {
|
|
53
63
|
if (nodes[index].elementId === target)
|
|
@@ -157,8 +167,113 @@ async function applyEventRemovals(platform, project, environment, applicationId,
|
|
|
157
167
|
await platform.tenant('PUT', project, `/pages/${applicationId}/${pageId}`, { environment, body });
|
|
158
168
|
return removed;
|
|
159
169
|
}
|
|
160
|
-
|
|
170
|
+
function findParentNode(nodes, target) {
|
|
171
|
+
for (const node of nodes) {
|
|
172
|
+
if ((node.children ?? []).some(child => child.elementId === target))
|
|
173
|
+
return node;
|
|
174
|
+
const deeper = findParentNode(node.children ?? [], target);
|
|
175
|
+
if (deeper)
|
|
176
|
+
return deeper;
|
|
177
|
+
}
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* `resize_component` gateway emülasyonudur: platformun micro-op sözlüğünde boyut YOKTUR
|
|
182
|
+
* (`move_component` yalnız `position` uygular, gövdeye konan `size` 200 döner ve sessizce düşer —
|
|
183
|
+
* ölçüldü 09-04). Bu yüzden kurulmuş bir sayfada bir kartın yüksekliğini değiştirmenin tek yolu
|
|
184
|
+
* düğümü `remove_component` + `add_component` ile yeniden kurmaktı; o da ona yazan akış
|
|
185
|
+
* setter'larını sessizce öldürüyordu (FLOW-018). Op şemayı okur, hedefin boyutunu değiştirir ve
|
|
186
|
+
* sayfayı tam gövdeyle geri yazar — `remove_event` ile aynı desen.
|
|
187
|
+
*
|
|
188
|
+
* Izgarada boyut `blocks[].size`, flex'te `appearance` alanlarındadır; ikisi de kabul edilir ve
|
|
189
|
+
* ebeveynin düzeni okunanı etkisiz kılıyorsa uyarı üretilir (PAGE-015 ile aynı yaklaşım: reddetme,
|
|
190
|
+
* ama sessiz de kalma).
|
|
191
|
+
*/
|
|
192
|
+
/**
|
|
193
|
+
* G-25 — `states` bir prop değil düğüm alanı: `replace_prop` yazamıyor, `move_component` yalnız
|
|
194
|
+
* konum uyguluyor ve `resize_component` (G-14) boyutla sınırlıydı. Kurulmuş bir düğümün
|
|
195
|
+
* `isWrapper` bayrağını değiştirmenin tek yolu düğümü silip yeniden kurmaktı; 09-05 koşusunda
|
|
196
|
+
* bunun bedeli 20 setter'ın sessizce kopmasıydı (G-21). Anahtar kümesi beyaz listeyle sınırlı:
|
|
197
|
+
* `states` açık uçlu bir sözlük ve uydurma bayrak sessizce kaydedilirdi.
|
|
198
|
+
*/
|
|
199
|
+
const WRITABLE_STATES = new Set(['isWrapper', 'isScrollable', 'isSticky', 'isDroppable', 'isFloating', 'isQuickStack']);
|
|
200
|
+
async function applyResizes(platform, project, environment, applicationId, pageId, page, ops) {
|
|
201
|
+
const schema = JSON.parse(JSON.stringify(page.schema ?? []));
|
|
202
|
+
const applied = [];
|
|
203
|
+
const warnings = [];
|
|
204
|
+
for (const op of ops) {
|
|
205
|
+
const target = String(op.target ?? '');
|
|
206
|
+
const size = op.size && typeof op.size === 'object' ? op.size : undefined;
|
|
207
|
+
const appearance = op.appearance && typeof op.appearance === 'object' ? op.appearance : undefined;
|
|
208
|
+
const states = op.states && typeof op.states === 'object' ? op.states : undefined;
|
|
209
|
+
const breakpoint = Number(op.breakpoint ?? 1024);
|
|
210
|
+
if (!target)
|
|
211
|
+
throw new PlatformApiError("resize_component için 'target' zorunludur.", 400);
|
|
212
|
+
if (!size && !appearance && !states) {
|
|
213
|
+
throw new PlatformApiError(`resize_component '${target}' için 'size' ({ w, h } — ızgara), 'appearance' ({ widthType, widthString, heightType, heightString } — flex) ya da 'states' ({ isWrapper, isScrollable, isSticky, isDroppable, isFloating, isQuickStack }) verilmelidir; üçü de boş.`, 400);
|
|
214
|
+
}
|
|
215
|
+
if (states) {
|
|
216
|
+
const unknownStates = Object.keys(states).filter(key => !WRITABLE_STATES.has(key));
|
|
217
|
+
if (unknownStates.length > 0) {
|
|
218
|
+
throw new PlatformApiError(`resize_component '${target}': bilinmeyen states anahtarı (${unknownStates.join(', ')}). Yazılabilir bayraklar: ${[...WRITABLE_STATES].join(', ')}.`, 400);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const node = findNode(schema, target);
|
|
222
|
+
if (!node)
|
|
223
|
+
throw new PlatformApiError(`resize_component hedefi bulunamadı: '${target}'. page_schema_get ile elementId'leri listeleyin.`, 400);
|
|
224
|
+
const parent = findParentNode(schema, target);
|
|
225
|
+
const inFlex = parent !== undefined && isFlexContainer(parent);
|
|
226
|
+
if (size) {
|
|
227
|
+
const blocks = Array.isArray(node.blocks) ? node.blocks : [];
|
|
228
|
+
const block = blocks.find(entry => Number(entry?.breakpoint) === breakpoint);
|
|
229
|
+
if (!block) {
|
|
230
|
+
throw new PlatformApiError(`resize_component '${target}' düğümünde ${breakpoint} breakpoint'li blok yok (var olanlar: ${blocks.map(entry => entry?.breakpoint).join(', ') || 'hiç'}). Farklı bir breakpoint verin ya da düğümü add_component ile blokları kurulu olarak ekleyin.`, 400);
|
|
231
|
+
}
|
|
232
|
+
const current = (block.size ?? {});
|
|
233
|
+
block.size = { w: Number(size.w ?? current.w ?? 0), h: Number(size.h ?? current.h ?? 0) };
|
|
234
|
+
applied.push(`${target}.size=${JSON.stringify(block.size)}`);
|
|
235
|
+
if (inFlex) {
|
|
236
|
+
warnings.push(`PAGE-019 (uyarı): '${target}' düğümünün ebeveyni flex kapsayıcı; orada yerleşimi 'blocks[].size' DEĞİL appearance (widthType/widthString/heightType/heightString) belirler. Boyut yazıldı ama ekranda etkisi olmayabilir — flex'te ölçü vermek için aynı op'a 'appearance' koyun.`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (states) {
|
|
240
|
+
node.states = { ...(node.states ?? {}), ...states };
|
|
241
|
+
applied.push(`${target}.states=${JSON.stringify(states)}`);
|
|
242
|
+
if (states.isWrapper === true) {
|
|
243
|
+
warnings.push(`PAGE-020 (uyarı): '${target}' düğümüne isWrapper:true yazıldı — runtime sarmalayıcı düğüme 'min-height: 80vh' veriyor ve bu, sayfanın altına yüzlerce piksel ölü boşluk ekleyebilir (ölçüldü 09-05: dokuz düğüm ~1600px). style ile ezilemez; gerekmiyorsa isWrapper:false bırakın.`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (appearance) {
|
|
247
|
+
node.appearance = { ...(node.appearance ?? {}), ...appearance };
|
|
248
|
+
applied.push(`${target}.appearance=${JSON.stringify(appearance)}`);
|
|
249
|
+
if (!inFlex && parent !== undefined) {
|
|
250
|
+
warnings.push(`PAGE-019 (uyarı): '${target}' düğümünün ebeveyni flex DEĞİL; ızgarada yerleşimi 'blocks[].size' belirler ve appearance genişlik/yükseklik alanları etkisiz kalabilir. Izgarada ölçü vermek için aynı op'a 'size' koyun.`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (applied.length === 0)
|
|
255
|
+
return { applied, warnings };
|
|
256
|
+
const body = pageUpdateBody(page, { schema });
|
|
257
|
+
await platform.tenant('PUT', project, `/pages/${applicationId}/${pageId}`, { environment, body });
|
|
258
|
+
return { applied, warnings };
|
|
259
|
+
}
|
|
260
|
+
async function applyPropTypes(platform, project, environment, applicationId, pageId, page, ops) {
|
|
261
|
+
const schema = JSON.parse(JSON.stringify(page.schema ?? []));
|
|
262
|
+
const { applied, missingTargets } = applyPropTypesToSchema(schema, ops);
|
|
263
|
+
if (missingTargets.length > 0) {
|
|
264
|
+
throw new PlatformApiError(`set_prop_type hedefi bulunamadı: '${missingTargets[0]}'. page_schema_get ile elementId'leri listeleyin.`, 400);
|
|
265
|
+
}
|
|
266
|
+
if (applied.length === 0)
|
|
267
|
+
return [];
|
|
268
|
+
const body = pageUpdateBody(page, { schema });
|
|
269
|
+
await platform.tenant('PUT', project, `/pages/${applicationId}/${pageId}`, { environment, body });
|
|
270
|
+
return applied;
|
|
271
|
+
}
|
|
272
|
+
export function registerPageTools(server, platform, injectedScreenshotter) {
|
|
161
273
|
const config = platform.config;
|
|
274
|
+
const asUserParam = z.string().min(1).optional().describe("Uygulama üyesinin kimliği: istek gateway'in servis jetonu yerine bu kimlikle gider. Jeton HEM Authorization başlığında HEM NG_AccessToken çerezinde taşınır, böylece çerez okuyan middleware de geçer. İKİ ŞEKİL kabul edilir: (1) ham JWT, (2) `tok_…` referansı — uygulamanın giriş ucunu microservice_execute ile çağırdığınızda jeton maskelenir ama yanıtın `tokenRefs` alanında referansı döner; jetonu hiç görmeden onu buraya verin (15 dk geçerli). anonymous ile birlikte verilemez.");
|
|
275
|
+
/** Sayfa ve uç araçları aynı kimlik sözleşmesini paylaşır (G-20); çözüm tokenVault'ta yaşar. */
|
|
276
|
+
const resolveIdentity = resolveIdentityToken;
|
|
162
277
|
server.registerTool('page_list', {
|
|
163
278
|
title: 'Sayfaları listele',
|
|
164
279
|
description: 'Uygulamanın sayfalarını (id, name, slug) listeler; hedef sayfayı bulmak için ilk adım.',
|
|
@@ -192,6 +307,7 @@ export function registerPageTools(server, platform) {
|
|
|
192
307
|
name: page.name,
|
|
193
308
|
slug: page.slug,
|
|
194
309
|
browserTitle: page.browserTitle ?? '',
|
|
310
|
+
browserTitleKey: page.browserTitleKey ?? '',
|
|
195
311
|
isHomepage: page.isHomepage === true,
|
|
196
312
|
layoutId: page.layoutId ?? layout.id ?? null,
|
|
197
313
|
layoutName: layout.name,
|
|
@@ -216,13 +332,20 @@ export function registerPageTools(server, platform) {
|
|
|
216
332
|
pageSlug: z.string().min(1).describe("Sayfanın slug'ı (page_list)"),
|
|
217
333
|
view: z.enum(['summary', 'full']).optional().describe('summary (varsayılan): şema/layout ağaçları id/name/type özetine indirgenir. full: ham gövde — büyük olabilir'),
|
|
218
334
|
resolveStyles: z.boolean().optional().describe("summary görünümünde her düğüme EFEKTİF stilini ekler: tema componentStyles → preset → düğümün kendi stili kaskadı çözülür (aibox'ta tipli stil grupları da derlenir ve TABAN katman olur). styleLayers hangi katmanın konuştuğunu söyler — 'bu kutu neden böyle görünüyor' sorusunun cevabı budur."),
|
|
219
|
-
anonymous: z.boolean().optional().describe("true: istek HİÇBİR Authorization header'ı olmadan gider — 'korumalı sayfa tokensız ne döner, şema sızıyor mu?' sorusunun cevabı budur. Korumalı sayfada middlewareBlocked bekleyin.")
|
|
335
|
+
anonymous: z.boolean().optional().describe("true: istek HİÇBİR Authorization header'ı ve çerezi olmadan gider — 'korumalı sayfa tokensız ne döner, şema sızıyor mu?' sorusunun cevabı budur. Korumalı sayfada middlewareBlocked bekleyin."),
|
|
336
|
+
asUser: asUserParam
|
|
220
337
|
},
|
|
221
338
|
annotations: readOnly
|
|
222
|
-
}, handle(config, async ({ project, environment, applicationSlug, pageSlug, view, resolveStyles, anonymous }) => {
|
|
339
|
+
}, handle(config, async ({ project, environment, applicationSlug, pageSlug, view, resolveStyles, anonymous, asUser }) => {
|
|
223
340
|
let response;
|
|
341
|
+
if (anonymous === true && asUser !== undefined) {
|
|
342
|
+
throw new PlatformApiError('page_runtime_get: anonymous:true ile asUser birlikte verilemez — biri kimliksiz isteği, diğeri belirli bir üyenin kimliğiyle atılan isteği ister.');
|
|
343
|
+
}
|
|
344
|
+
// page_screenshot çerez, bu tool başlık taşıyordu; çerez okuyan middleware ikincisinden
|
|
345
|
+
// yapısal olarak geçemiyordu. Kimlik artık iki kanalda birden gider (Y-02).
|
|
346
|
+
const identity = asUser === undefined ? {} : { headers: { authorization: `Bearer ${resolveIdentity(asUser)}` } };
|
|
224
347
|
try {
|
|
225
|
-
response = await platform.tenant('GET', project, `/${applicationSlug}/p/${pageSlug}`, { environment, anonymous });
|
|
348
|
+
response = await platform.tenant('GET', project, `/${applicationSlug}/p/${pageSlug}`, { environment, anonymous, mirrorAuthorizationCookie: true, ...identity });
|
|
226
349
|
}
|
|
227
350
|
catch (error) {
|
|
228
351
|
if (error instanceof PlatformApiError && typeof error.status === 'number' && error.status >= 300) {
|
|
@@ -245,7 +368,7 @@ export function registerPageTools(server, platform) {
|
|
|
245
368
|
const asNodes = (value) => (Array.isArray(value) ? value : []);
|
|
246
369
|
const named = (value) => (Array.isArray(value) ? value : []).map(entry => ({ id: entry.id, name: entry.name, slug: entry.slug }));
|
|
247
370
|
return {
|
|
248
|
-
page: { id: body.id, name: body.name, slug: body.slug, browserTitle: body.browserTitle, isHomepage: body.isHomepage },
|
|
371
|
+
page: { id: body.id, name: body.name, slug: body.slug, browserTitle: body.browserTitle, browserTitleKey: body.browserTitleKey, isHomepage: body.isHomepage },
|
|
249
372
|
events: body.events ?? [],
|
|
250
373
|
layout: layout ? { id: layout.id, name: layout.name, views: asNodes(layout.views).map(node => summarize(node, styleSource)) } : null,
|
|
251
374
|
schema: asNodes(body.schema).map(node => summarize(node, styleSource)),
|
|
@@ -258,11 +381,11 @@ export function registerPageTools(server, platform) {
|
|
|
258
381
|
: "schema, layout'taki slot'un İÇİNE yerleşerek çizilir (Kural 1f); tam gövde gerekirse view:'full'. Bir düğümün ekranda GERÇEKTEN aldığı stili görmek için resolveStyles:true."
|
|
259
382
|
};
|
|
260
383
|
}));
|
|
261
|
-
const screenshotter = new StudioScreenshotter(config);
|
|
384
|
+
const screenshotter = injectedScreenshotter ?? new StudioScreenshotter(config);
|
|
262
385
|
const maxInlinePngBytes = 5 * 1024 * 1024;
|
|
263
386
|
server.registerTool('page_screenshot', {
|
|
264
387
|
title: 'Sayfanın ekran görüntüsünü al',
|
|
265
|
-
description: "Studio arayüzünün runtime render rotasını (/dev ya da /view) headless Chromium'da açar ve sayfanın PNG ekran görüntüsünü inline görüntü olarak döner — kurduğun sayfayı GÖZLE doğrulamanın yolu budur (page_runtime_get paketi verir, bu tool ekranda ne ÇİZİLDİĞİNİ gösterir). Sayfaya bağlı middleware'ler gerçekten koşar; reddedilirse yanıtta middlewareBlocked işaretlenir ve hata
|
|
388
|
+
description: "Studio arayüzünün runtime render rotasını (/dev ya da /view) headless Chromium'da açar ve sayfanın PNG ekran görüntüsünü inline görüntü olarak döner — kurduğun sayfayı GÖZLE doğrulamanın yolu budur (page_runtime_get paketi verir, bu tool ekranda ne ÇİZİLDİĞİNİ gösterir). Sayfaya bağlı middleware'ler gerçekten koşar; reddedilirse yanıtta middlewareBlocked işaretlenir ve Studio'nun çizdiği hata durumu karesi döner — bu da bir doğrulamadır. YÜKLENMEMİŞ SAYFA ASLA ÇEKİLMEZ: kare, runtime çizilip mount akışları bitince alınır (yanıtta pageState + readyAfterMs); bütçe dolduğunda sayfa hâlâ iskelette ya da akış koşuyorsa çağrı ne görüldüğünü söyleyen bir hatayla döner, boş/yarım kare dönmez. Sayfa akışı bir adımda düşerse (patlayan ifade, hata dönen uç) yanıt runtimeErrors taşır ve hangi komponentin hangi prop'unun YAZILMADIĞINI söyler — kare 'çalışıyor' görünürken sessizce boş kalan komponenti ancak bu yakalar. STUDIO_BASE_URL tanımlı olmalı; ilk kullanım için `npx playwright-core install chromium` gerekir.",
|
|
266
389
|
inputSchema: {
|
|
267
390
|
project: projectParam,
|
|
268
391
|
environment: environmentParam,
|
|
@@ -273,13 +396,27 @@ export function registerPageTools(server, platform) {
|
|
|
273
396
|
height: z.number().int().min(240).max(2160)
|
|
274
397
|
}).optional().describe('Varsayılan 1280x800 (deviceScaleFactor 1)'),
|
|
275
398
|
fullPage: z.boolean().optional().describe('true: kaydırılan kısım dahil tüm sayfa; varsayılan yalnız viewport karesi'),
|
|
276
|
-
delayMs: z.number().int().min(0).max(
|
|
277
|
-
|
|
399
|
+
delayMs: z.number().int().min(0).max(60000).optional().describe("Hazırlık bütçesine EK süre (ms). Varsayılan bütçe 15 sn'dir ve kare sayfa hazır olur olmaz alınır (akışsız giriş ekranı ~1 sn, dashboard mount akışları bitince) — normalde vermeye gerek yok. Yalnız sayfa gerçekten uzun sürüyorsa (soğuk Vite derlemesi, ağır mount akışı) büyütün: bütçe dolduğunda sayfa hâlâ hazır değilse çağrı hatayla döner, kare alınmaz. Sayfanın AÇILMASI için beklenen 20 sn'lik navigasyon süresi de bu kadar uzar — soğuk Studio'da tek düğme budur (09-08 S-8)."),
|
|
400
|
+
captureUnready: z.boolean().optional().describe("true: TEŞHİS kipi — bütçe dolduğunda sayfa hazır olmasa da kare alınır ve yanıt pageState: 'not-ready' taşır (iskelet/yarım kare görülebilir). Varsayılan false: yüklenmemiş sayfa çekilmez, hata döner."),
|
|
401
|
+
anonymous: z.boolean().optional().describe("true: hiçbir çerez enjekte edilmez — anonim ziyaretçinin gördüğü kare. Korumalı sayfada middlewareBlocked bekleyin."),
|
|
402
|
+
asUser: asUserParam,
|
|
403
|
+
expectFailure: z.boolean().optional().describe("true: bu çekimde bir akış adımının düşmesi BEKLENİYOR (kasıtlı negatif test — yanlış parola, yetkisiz kullanıcı, geçersiz gövde). Kare yine alınır ve runtimeErrors yine dolar, ama yanıt expectedFailure:true taşır ve benchmark raporu bu satırı plansız hata saymaz: 2026-09-06 koşusunda on başarısız çağrının ALTISI kasıtlı negatif testti ve ayıklama elle yapıldı (S-4)."),
|
|
404
|
+
layoutReport: z.boolean().optional().describe("true: kareyle birlikte her runtime düğümünün efektif piksel kutusunu ve özetini (taşan / viewport dışı / sıfır boyutlu elementId'ler) döner — taşma ve çakışmayı görüntüsüz, sayısal yakalar. Düğümler data-element-id taşımıyorsa layoutReportUnavailable döner."),
|
|
405
|
+
cookies: z.record(z.string()).optional().describe("Tarayıcıya enjekte edilecek EK çerezler (ad → değer): uygulamanın kendi oturum çerezi buraya verilir. Aynı ada sahip çerez gateway jetonunun yerine geçer. anonymous ile birlikte verilemez."),
|
|
406
|
+
actions: z.array(z.object({
|
|
407
|
+
action: z.enum(['set', 'click', 'wait']),
|
|
408
|
+
target: z.string().min(1).optional().describe("Hedefin elementId'si (page_schema_get); runtime düğümü data-element-id ile bulunur"),
|
|
409
|
+
value: z.unknown().optional().describe("'set' için yazılacak değer"),
|
|
410
|
+
ms: z.number().int().min(0).max(15000).optional().describe("'wait' için bekleme süresi")
|
|
411
|
+
})).max(20).optional().describe("Kare ALINMADAN ÖNCE sayfada koşturulacak etkileşim adımları: [{action:'set', target:'flt_month', value:'2022-10'}, {action:'click', target:'btn_sign_in'}]. 'set' native setter kullanır ve input+change olaylarını bubbles ile atar (düz DOM ataması framework'ün duyduğu olayı üretmez). Her adımdan sonra yerleşme beklenir ve yanıttaki actionsReport hedefin bulunup bulunmadığını ve adımdan sonra KAÇ AĞ İSTEĞİ çıktığını söyler — 'bağ kuruldu' ile 'çalışıyor' arasındaki farkı ancak bu ayırır (G-24)."),
|
|
278
412
|
outPath: z.string().min(1).optional().describe('Verilirse PNG bu yerel yola yazılır ve inline görüntü DÖNMEZ; yalnız özet (yol, boyut) döner — büyük fullPage kareleri için')
|
|
279
413
|
},
|
|
280
414
|
annotations: readOnly
|
|
281
|
-
}, async ({ project, environment, applicationSlug, pageSlug, viewport, fullPage, delayMs, anonymous, outPath }) => {
|
|
415
|
+
}, async ({ project, environment, applicationSlug, pageSlug, viewport, fullPage, delayMs, captureUnready, anonymous, asUser, cookies, layoutReport, actions, outPath, expectFailure }) => {
|
|
282
416
|
try {
|
|
417
|
+
if (anonymous === true && (asUser !== undefined || cookies !== undefined)) {
|
|
418
|
+
throw new PlatformApiError("page_screenshot: anonymous:true ile asUser/cookies birlikte verilemez — biri kimliksiz kareyi, diğeri belirli bir kimlikle çekilen kareyi ister. Anonim kare için yalnız anonymous:true, üye kimliğiyle kare için yalnız asUser (ve gerekiyorsa cookies) gönderin.");
|
|
419
|
+
}
|
|
283
420
|
if (!config.studioBaseUrl) {
|
|
284
421
|
throw new PlatformApiError("page_screenshot için STUDIO_BASE_URL tanımlı değil. Studio arayüzünün kök adresini .env'e ekleyin (yerel geliştirmede http://localhost:3000, dağıtımda nginx adresi).");
|
|
285
422
|
}
|
|
@@ -290,33 +427,103 @@ export function registerPageTools(server, platform) {
|
|
|
290
427
|
}
|
|
291
428
|
const url = `${config.studioBaseUrl}/${lane}/${platform.resolveProject(project)}/${applicationSlug}/${pageSlug}`;
|
|
292
429
|
const size = viewport ?? { width: 1280, height: 800 };
|
|
430
|
+
const extraCookies = Object.entries(cookies ?? {}).map(([name, value]) => ({ name, value }));
|
|
431
|
+
const identity = anonymous === true ? 'anonymous' : asUser === undefined ? 'gateway' : 'asUser';
|
|
432
|
+
const cookieNames = [...new Set([...(identity === 'anonymous' ? [] : ['NG_AccessToken']), ...extraCookies.map(cookie => cookie.name)])];
|
|
293
433
|
const shot = await screenshotter.capture({
|
|
294
434
|
url,
|
|
295
435
|
apiPathMarker: `/${applicationSlug}/p/${pageSlug}`,
|
|
296
|
-
token: anonymous ? undefined : await platform.accessToken(),
|
|
436
|
+
token: anonymous ? undefined : resolveIdentity(asUser) ?? await platform.accessToken(),
|
|
437
|
+
...(extraCookies.length > 0 ? { cookies: extraCookies } : {}),
|
|
297
438
|
cookieUrl: config.studioBaseUrl,
|
|
298
439
|
viewport: size,
|
|
299
440
|
fullPage: fullPage === true,
|
|
300
|
-
delayMs: delayMs ?? 0
|
|
441
|
+
delayMs: delayMs ?? 0,
|
|
442
|
+
...(captureUnready === true ? { captureUnready: true } : {}),
|
|
443
|
+
...(layoutReport === true ? { layoutReport: true } : {}),
|
|
444
|
+
...(actions === undefined ? {} : { actions: actions })
|
|
301
445
|
});
|
|
302
446
|
const meta = {
|
|
303
447
|
url,
|
|
304
448
|
environment: resolvedEnvironment,
|
|
305
449
|
viewport: size,
|
|
306
450
|
fullPage: fullPage === true,
|
|
451
|
+
identity,
|
|
452
|
+
cookieNames,
|
|
307
453
|
sizeBytes: shot.png.byteLength,
|
|
308
454
|
elapsedMs: shot.elapsedMs,
|
|
455
|
+
...(shot.contentHeight === undefined ? {} : { contentHeight: shot.contentHeight }),
|
|
456
|
+
...(shot.fullPageTruncated
|
|
457
|
+
? { fullPageTruncated: true, note: `Sayfa içeriği ${shot.contentHeight} px — kare 16384 px'de kırpıldı. Alt kısmı görmek için viewport'u kaydırılmış bir bölüme odaklayın ya da sayfayı bölerek doğrulayın.` }
|
|
458
|
+
: {}),
|
|
309
459
|
...(shot.apiStatus === undefined ? {} : { apiStatus: shot.apiStatus }),
|
|
310
460
|
...(shot.apiStatus !== undefined && shot.apiStatus >= 300
|
|
311
|
-
? { middlewareBlocked: true, note:
|
|
461
|
+
? { middlewareBlocked: true, note: `Sayfanın veri isteği reddedildi — karede hata/boş durum görünüyor olabilir. Korumalı sayfada kimliksiz/yetkisiz isteğin reddedilmesi BEKLENEN davranıştır (page_runtime_get ile karşılaştırın).${identity === 'gateway' ? " Gateway'in servis jetonu uygulamanın kendi kullanıcı tablosuna çözülmeyebilir; üye kimliğiyle çekmek için asUser (ve gerekiyorsa cookies) verin. Uygulamanın 'users' tablosu boşsa hiçbir kimlik çözülmez; yeni projede o tablo BOŞ gelir — row_list('users') ile bakın, ilk kullanıcıyı açma yolu platform_guide('getting-started') §'İlk kullanıcıyı kim açar?'." : ''}` }
|
|
462
|
+
: {}),
|
|
463
|
+
// P1-1 (S-3): eşzamanlı çekimler gateway içinde SERİ koşar; ajan yavaşlığın kuyruktan mı
|
|
464
|
+
// yoksa sayfadan mı geldiğini bilsin.
|
|
465
|
+
...(shot.queuedMs === undefined ? {} : { queuedMs: shot.queuedMs }),
|
|
466
|
+
...(shot.contextRecycled === true
|
|
467
|
+
? { contextRecycled: true, contextRecycledNote: 'Önceki çekim tarayıcı bağlamını bozdu; bu kare TEMİZ bir tarayıcıyla alındı — oturum çerezleri yeniden enjekte edildi.' }
|
|
312
468
|
: {}),
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
469
|
+
// S-8 (09-08): soğuk Studio'da navigasyon zaman aşımı ajanın çağrıyı tekrarlamasını
|
|
470
|
+
// gerektiriyordu; artık gateway bir kez kendi deniyor ve karenin neden geciktiğini söylüyor.
|
|
471
|
+
...(shot.navigationRetried === true
|
|
472
|
+
? { navigationRetried: true, navigationRetriedNote: 'İlk navigasyon zaman aşımına düştü ve kare ikinci denemede alındı — Studio soğuktu (ilk istek Vite derlemesini ısıtır). Sonraki çekimler hızlı olmalı; olmuyorsa delayMs verin, o değer navigasyon bütçesini de uzatır.' }
|
|
473
|
+
: {}),
|
|
474
|
+
// B-6 (2026-09-07): kare yalnız sayfa hazırken alınır; hangi kanıtla hazır sayıldığı yazılır.
|
|
475
|
+
pageState: shot.pageState,
|
|
476
|
+
readyAfterMs: shot.readyAfterMs,
|
|
477
|
+
...(shot.finalUrl === undefined ? {} : { finalUrl: shot.finalUrl }),
|
|
478
|
+
...(shot.pageState === 'ready-no-flows'
|
|
479
|
+
? { pageStateNote: 'Runtime çizildi ama hiç mount akışı koşmadı. Sayfa akışsızsa (giriş ekranı) normaldir; veri bekliyorsanız page_schema_get ile mount olayına bağlı akış olup olmadığına bakın.' }
|
|
480
|
+
: shot.pageState === 'ready-no-counter'
|
|
481
|
+
? { pageStateNote: 'Runtime koşan akış sayacı yayınlamıyor (eski runtime); hazırlık DOM durulmasından çıkarıldı, akışların bittiği kanıtlanamadı.' }
|
|
482
|
+
: shot.pageState === 'error'
|
|
483
|
+
? { pageStateNote: 'Studio sayfa için hata/bulunamadı durumunu çizdi; kare o durumu gösterir. apiStatus ve slug\'ları doğrulayın.' }
|
|
484
|
+
: shot.pageState === 'redirected'
|
|
485
|
+
? { pageStateNote: `İstenen adres runtime dışına yönlendi (finalUrl). Route guard ya da Studio giriş ekranı olabilir; kare yönlendirilen sayfayı gösterir.` }
|
|
486
|
+
: shot.pageState === 'not-ready'
|
|
487
|
+
? { pageStateNote: 'TEŞHİS KARESİ (captureUnready): bütçe dolduğunda sayfa hazır değildi; kare iskelet ya da yarım durumu gösterebilir, doğrulama için kullanmayın.' }
|
|
488
|
+
: {}),
|
|
489
|
+
...(shot.renderWaitTimedOut && shot.pageState === 'not-ready'
|
|
490
|
+
? { renderWaitTimedOut: true, note: "Sayfanın veri isteği gözlenmedi — STUDIO_BASE_URL ve slug'ları doğrulayın." }
|
|
491
|
+
: {}),
|
|
492
|
+
...(shot.flowsSettled === undefined
|
|
493
|
+
? {}
|
|
494
|
+
: shot.flowsSettled
|
|
495
|
+
? { flowsSettled: true }
|
|
496
|
+
: { flowsSettled: false, flowsSettledNote: 'Sayfanın akışları bütçe içinde bitmedi; kare akış koşarken alındı (captureUnready). Akış gerçekten uzunsa delayMs ile bütçeyi büyütün.' }),
|
|
497
|
+
...(shot.actions === undefined ? {} : { actionsReport: shot.actions }),
|
|
498
|
+
// S-4: kasıtlı negatif test de akış hatası bırakır ve ölçüm aracı onu plansız hata
|
|
499
|
+
// sayıyordu. Çağıran beklediğini söylediğinde satır ayrı sayılır.
|
|
500
|
+
...(expectFailure === true ? { expectedFailure: true } : {}),
|
|
501
|
+
...(shot.runtimeErrors === undefined
|
|
502
|
+
? {}
|
|
503
|
+
: {
|
|
504
|
+
runtimeErrors: shot.runtimeErrors,
|
|
505
|
+
runtimeErrorsNote: `Sayfa akışı ${shot.runtimeErrors.length} yapısal hata bıraktı; kare 'çalışıyor' görünse bile bu adımlar işini YAPMADI. element/property alanı hangi komponentin hangi prop'unun yazılamadığını söyler — o komponent eski/boş değerindedir. Adımı flow_get ile açıp ifadeyi düzeltin.`
|
|
506
|
+
}),
|
|
507
|
+
...(shot.actions?.some(outcome => !outcome.found)
|
|
508
|
+
? { actionsNote: `Bulunamayan hedef var: ${shot.actions.filter(outcome => !outcome.found).map(outcome => outcome.target ?? '?').join(' · ')}. elementId'leri page_schema_get ile doğrulayın; runtime düğümü data-element-id ile bulunuyor.` }
|
|
509
|
+
: {}),
|
|
510
|
+
...(shot.actions?.some(outcome => outcome.applied && outcome.requests === 0 && (outcome.flowRuns ?? 0) === 0)
|
|
511
|
+
? { actionsNote: `Uygulandı ama HİÇ AĞ İSTEĞİ çıkmadı ve HİÇBİR AKIŞ KOŞMADI: ${shot.actions.filter(outcome => outcome.applied && outcome.requests === 0 && (outcome.flowRuns ?? 0) === 0).map(outcome => `${outcome.action} '${outcome.target ?? '?'}'`).join(' · ')}. Olay bağı şemada görünse bile akış koşmamış olabilir — bağın gerçekten kurulu olduğunu page_runtime_get ile, akışın gövdesini flow_get ile doğrulayın.` }
|
|
512
|
+
: {}),
|
|
513
|
+
...(shot.layout === undefined
|
|
514
|
+
? {}
|
|
515
|
+
: shot.layout.length === 0
|
|
516
|
+
? { layoutReportUnavailable: true, note: "Sayfada data-element-id taşıyan düğüm yok — runtime bu attribute'u henüz basmıyor olabilir (F-04) ya da sayfa boş render edildi." }
|
|
517
|
+
: {
|
|
518
|
+
layoutSummary: summarizeLayout(shot.layout, size),
|
|
519
|
+
...(shot.layout.length <= 100 ? { layout: shot.layout } : { layoutTruncatedTo: 'summary' })
|
|
520
|
+
})
|
|
316
521
|
};
|
|
317
522
|
if (outPath) {
|
|
318
523
|
const absolute = resolve(outPath);
|
|
319
524
|
try {
|
|
525
|
+
// B-6: klasör yoksa ENOENT ile düşüyordu; koşu ajanı her seferinde klasörü elle açıyordu.
|
|
526
|
+
await mkdir(dirname(absolute), { recursive: true });
|
|
320
527
|
await writeFile(absolute, shot.png);
|
|
321
528
|
}
|
|
322
529
|
catch (error) {
|
|
@@ -376,10 +583,16 @@ export function registerPageTools(server, platform) {
|
|
|
376
583
|
server.registerTool('page_schema_validate', {
|
|
377
584
|
title: 'Sayfa şemasını doğrula (kuru çalıştırma)',
|
|
378
585
|
description: "Micro-op'ları kaydetmeden şemaya uygular ve doğrulama hatalarını (suggestions ile) döndürür; page_schema_patch öncesi planı test etmek için. Gövdeye page_schema_patch ile AYNI normalizasyonu uygular (Prop sarmalı, validation biçimi) — kuru koşu ile yazma aynı gövdeyi görür.",
|
|
379
|
-
inputSchema: { project: projectParam, environment: environmentParam, applicationId: guid, pageId: guid, ops: opsParam.optional() },
|
|
586
|
+
inputSchema: { project: projectParam, environment: environmentParam, applicationId: guid, pageId: guid, ops: opsParam.optional(), operations: opsParam.optional().describe("'ops' ile aynı alan (eşanlamlı) — entity/microservice/workflow patch'leriyle aynı adı kullanabilmeniz için") },
|
|
380
587
|
annotations: readOnly
|
|
381
|
-
}, handle(config, async ({ project, environment, applicationId, pageId, ops }) => {
|
|
382
|
-
const
|
|
588
|
+
}, handle(config, async ({ project, environment, applicationId, pageId, ops, operations }) => {
|
|
589
|
+
const allOps = microOps('page_schema_validate', ops, operations).map(op => op);
|
|
590
|
+
const propTypeIssues = setPropTypeErrors(allOps.filter(entry => entry.op === 'set_prop_type'));
|
|
591
|
+
if (propTypeIssues.length > 0)
|
|
592
|
+
throw new PlatformApiError(`set_prop_type gövdesi eksik (istek gönderilmedi): ${propTypeIssues.join(' · ')}`, 400);
|
|
593
|
+
// Gateway emülasyonu olan op'lar platformun doğrulama ucunu tanımadığı için gövdeden ayrılır;
|
|
594
|
+
// kuru koşu ile yazma AYNI kararı vermeli (P0-2 Step 3).
|
|
595
|
+
const opRecords = allOps.filter(entry => !['remove_event', 'resize_component', 'set_prop_type'].includes(String(entry.op)));
|
|
383
596
|
const normalized = normalizeSchemaOps(opRecords);
|
|
384
597
|
const validationIssues = schemaOpValidationErrors(opRecords);
|
|
385
598
|
const response = ((await platform.tenant('POST', project, `/pages/${applicationId}/${pageId}/schema/validate`, {
|
|
@@ -396,22 +609,34 @@ export function registerPageTools(server, platform) {
|
|
|
396
609
|
}));
|
|
397
610
|
server.registerTool('page_schema_patch', {
|
|
398
611
|
title: 'Sayfa şemasına patch uygula',
|
|
399
|
-
description: "Micro-op'ları doğrulama duvarından geçirip kaydeder; açık studio canvas'ları SignalR ile canlı güncellenir. expectedUpdatedAt verilirse sayfa o okumadan beri değişmişse istek reddedilir (stale plan koruması). Yanıttaki appliedPatch, her op'un şemada tam olarak hangi JSON Pointer'a dokunduğunu söyler.",
|
|
612
|
+
description: "Micro-op'ları doğrulama duvarından geçirip kaydeder; açık studio canvas'ları SignalR ile canlı güncellenir. expectedUpdatedAt verilirse sayfa o okumadan beri değişmişse istek reddedilir (stale plan koruması). Yanıttaki appliedPatch, her op'un şemada tam olarak hangi JSON Pointer'a dokunduğunu söyler; gövdesi VARSAYILAN OLARAK iskelettir (eklenen düğüm id/type/children'a iner) — tam gövde için view:'full'. Op'un gerçekten kaydedilip kaydedilmediğini gövde değil silentlyDropped söyler.",
|
|
400
613
|
inputSchema: {
|
|
401
614
|
project: projectParam,
|
|
402
615
|
environment: environmentParam,
|
|
403
616
|
applicationId: guid,
|
|
404
617
|
pageId: guid,
|
|
405
|
-
ops: opsParam,
|
|
406
|
-
|
|
618
|
+
ops: opsParam.optional(),
|
|
619
|
+
operations: opsParam.optional().describe("'ops' ile aynı alan (eşanlamlı) — entity/microservice/workflow patch'leriyle aynı adı kullanabilmeniz için"),
|
|
620
|
+
expectedUpdatedAt: z.string().optional().describe("page_schema_get çıktısındaki updatedAt; eşleşmezse patch uygulanmaz"),
|
|
621
|
+
allowFlowDeletion: z.boolean().optional().describe("true: bu yama bir akışı referanssız bırakacaksa yine de uygulanır. Varsayılan false — platform referanssız akışı KALICI siler ve gateway isteği önden reddeder (G-21). Silinen akışlar yanıtta deletedFlows ile listelenir."),
|
|
622
|
+
view: writeViewParam
|
|
407
623
|
},
|
|
408
624
|
annotations: updates
|
|
409
|
-
}, handle(config, async ({ project, environment, applicationId, pageId, ops, expectedUpdatedAt }) => {
|
|
410
|
-
const allOps = ops.map(op => op);
|
|
625
|
+
}, handle(config, async ({ project, environment, applicationId, pageId, ops, operations, expectedUpdatedAt, allowFlowDeletion, view }) => {
|
|
626
|
+
const allOps = microOps('page_schema_patch', ops, operations).map(op => op);
|
|
411
627
|
const removeEventOps = allOps.filter(entry => entry.op === 'remove_event');
|
|
412
|
-
const
|
|
628
|
+
const resizeOps = allOps.filter(entry => entry.op === 'resize_component');
|
|
629
|
+
const propTypeOps = allOps.filter(entry => entry.op === 'set_prop_type');
|
|
630
|
+
const opRecords = allOps.filter(entry => !['remove_event', 'resize_component', 'set_prop_type'].includes(String(entry.op)));
|
|
631
|
+
const propTypeIssues = setPropTypeErrors(propTypeOps);
|
|
632
|
+
if (propTypeIssues.length > 0)
|
|
633
|
+
throw new PlatformApiError(`set_prop_type gövdesi eksik (istek gönderilmedi): ${propTypeIssues.join(' · ')}`, 400);
|
|
413
634
|
const containerOps = opRecords
|
|
414
635
|
.filter(entry => (entry.op === 'remove_component' || entry.op === 'move_component') && entry.target === PAGE_CONTAINER_ELEMENT_ID);
|
|
636
|
+
const sizedMoves = opRecords.filter(entry => entry.op === 'move_component' && entry.size !== undefined);
|
|
637
|
+
if (sizedMoves.length > 0) {
|
|
638
|
+
throw new PlatformApiError(`move_component yalnız 'position' uygular; gövdeye konan 'size' platformda SESSİZCE düşer (ölçüldü 2026-09-04, appliedPatch'te yalnız /blocks/0/position görünür). Boyut için resize_component kullanın: { op: 'resize_component', target: '<elementId>', size: { w, h } } — flex kapsayıcı içindeyse 'appearance'. Etkilenen hedefler: ${sizedMoves.map(entry => entry.target).join(' · ')}`, 400);
|
|
639
|
+
}
|
|
415
640
|
if (containerOps.length > 0) {
|
|
416
641
|
throw new PlatformApiError(`'${PAGE_CONTAINER_ELEMENT_ID}' sayfanın kök kapsayıcısıdır; silinmez ve taşınmaz (PAGE-006) — silinirse sayfa iskeleti bozulur ve Studio komponentleri kapsayıcı dışında bırakır. Komponent kaldırmak için remove_component'i kapsayıcının İÇİNDEKİ elementId'lerle kullanın (page_schema_get ile listeleyin).`, 400);
|
|
417
642
|
}
|
|
@@ -455,23 +680,27 @@ export function registerPageTools(server, platform) {
|
|
|
455
680
|
if (unknown.length > 0)
|
|
456
681
|
throw new PlatformApiError(`PAGE-008 — geçersiz olay adı (istek gönderilmedi): ${unknown.join(' · ')}`, 400);
|
|
457
682
|
}
|
|
683
|
+
// Akış silen op'lar yamadan ÖNCE hesaplanır: uyarı sonradan geldiğinde akış zaten kalıcı
|
|
684
|
+
// silinmiş oluyordu ve "önce yedekle" tavsiyesi uygulanamıyordu (09-05 koşusu, G-21).
|
|
685
|
+
let deletedFlows = [];
|
|
686
|
+
if (opRecords.some(entry => ['remove_component', 'set_event', 'remove_event'].includes(String(entry.op)))) {
|
|
687
|
+
try {
|
|
688
|
+
const page = await readPage();
|
|
689
|
+
deletedFlows = orphanedFlowPlan(page.schema, page.events, [...opRecords, ...removeEventOps]);
|
|
690
|
+
}
|
|
691
|
+
catch {
|
|
692
|
+
deletedFlows = [];
|
|
693
|
+
}
|
|
694
|
+
if (deletedFlows.length > 0 && allowFlowDeletion !== true) {
|
|
695
|
+
throw new PlatformApiError(orphanedFlowMessage(deletedFlows), 400);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
458
698
|
const added = opRecords
|
|
459
699
|
.filter(entry => entry.op === 'add_component')
|
|
460
700
|
.map(entry => entry.component);
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
const badNames = invalidComponentNames(added);
|
|
465
|
-
if (badNames.length > 0) {
|
|
466
|
-
throw new PlatformApiError(`PAGE-009 — komponent adı JS tanımlayıcısı değil (istek gönderilmedi): ${badNames.join(' · ')}. `
|
|
467
|
-
+ 'Runtime her komponent için `const <ad> = Components["<ad>"]` üretir; boşluklu ya da geçersiz bir ad '
|
|
468
|
-
+ 'SAYFADAKİ TÜM ifadeleri "Missing initializer in const declaration" ile öldürür (layout adları her sayfaya sızar). '
|
|
469
|
-
+ 'name alanını elementId ile aynı yaz; okunabilir Türkçe başlık için props.title / props.text / props.label kullan.', 400);
|
|
470
|
-
}
|
|
471
|
-
const catalog = added.length > 0
|
|
472
|
-
? await applyCatalogLayoutRules(platform, project, environment, applicationId, added)
|
|
473
|
-
: { clamped: [], heights: [] };
|
|
474
|
-
const formWarnings = formBindingWarnings(opRecords);
|
|
701
|
+
assertScreenWriteGuards('page', { schema: added });
|
|
702
|
+
const catalogNotes = await screenCatalogNotes(platform, project, environment, applicationId, added);
|
|
703
|
+
const formWarnings = [...formBindingWarnings(opRecords), ...formSubmitButtonWarnings(opRecords)];
|
|
475
704
|
let flowWarnings = [];
|
|
476
705
|
let layoutWarnings = [];
|
|
477
706
|
if (opRecords.some(entry => entry.op === 'remove_component')) {
|
|
@@ -482,6 +711,16 @@ export function registerPageTools(server, platform) {
|
|
|
482
711
|
catch {
|
|
483
712
|
flowWarnings = [];
|
|
484
713
|
}
|
|
714
|
+
// Ters yön (FLOW-018): silinen düğüme YAZAN setter'lar. Akış gövdeleri şemada değil, ayrı
|
|
715
|
+
// uçta; okunamazsa uyarı üretmeyiz ama yama yine geçer.
|
|
716
|
+
try {
|
|
717
|
+
const hosted = await platform.tenant('GET', project, flowPath('page', applicationId, pageId), { environment });
|
|
718
|
+
const page = await readPage();
|
|
719
|
+
flowWarnings = [...flowWarnings, ...staleSetterWarnings(hosted?.data ?? [], opRecords, page.schema)];
|
|
720
|
+
}
|
|
721
|
+
catch {
|
|
722
|
+
// akış listesi okunamadı
|
|
723
|
+
}
|
|
485
724
|
}
|
|
486
725
|
let nestedAfterOps = [];
|
|
487
726
|
const simulatable = opRecords.some(entry => ['add_component', 'move_component'].includes(String(entry.op))
|
|
@@ -498,7 +737,8 @@ export function registerPageTools(server, platform) {
|
|
|
498
737
|
...pageContainerPositionWarning(page.schema),
|
|
499
738
|
...verticalGapWarnings(simulated, 'PAGE-010 (uyarı)', touched),
|
|
500
739
|
...stringStyleWarning(stringStyleNodes(simulated)),
|
|
501
|
-
...inertFlexPropWarning(inertFlexPropNodes(simulated))
|
|
740
|
+
...inertFlexPropWarning(inertFlexPropNodes(simulated)),
|
|
741
|
+
...flexSizeWarnings(simulated, 'PAGE-017 (uyarı)', 'PAGE-018 (uyarı)')
|
|
502
742
|
];
|
|
503
743
|
}
|
|
504
744
|
catch {
|
|
@@ -512,29 +752,62 @@ export function registerPageTools(server, platform) {
|
|
|
512
752
|
: [];
|
|
513
753
|
if (removeEventOps.length > 0)
|
|
514
754
|
cachedPage = undefined;
|
|
755
|
+
const resized = resizeOps.length > 0
|
|
756
|
+
? await applyResizes(platform, project, environment, applicationId, pageId, await readPage(), resizeOps)
|
|
757
|
+
: { applied: [], warnings: [] };
|
|
758
|
+
if (resizeOps.length > 0)
|
|
759
|
+
cachedPage = undefined;
|
|
760
|
+
const propTypes = propTypeOps.length > 0
|
|
761
|
+
? await applyPropTypes(platform, project, environment, applicationId, pageId, await readPage(), propTypeOps)
|
|
762
|
+
: [];
|
|
763
|
+
if (propTypeOps.length > 0)
|
|
764
|
+
cachedPage = undefined;
|
|
515
765
|
const response = opRecords.length > 0
|
|
516
766
|
? ((await platform.tenant('PATCH', project, `/pages/${applicationId}/${pageId}/schema`, { environment, body: { ops: opRecords } })) ?? {})
|
|
517
767
|
: { valid: true };
|
|
768
|
+
if (view !== 'full')
|
|
769
|
+
outlineWriteResponse(response);
|
|
518
770
|
if (removedEvents.length > 0)
|
|
519
771
|
response.removedEvents = removedEvents;
|
|
772
|
+
if (propTypes.length > 0)
|
|
773
|
+
response.propTypes = propTypes;
|
|
774
|
+
if (resized.applied.length > 0)
|
|
775
|
+
response.resized = resized.applied;
|
|
776
|
+
if (resized.warnings.length > 0)
|
|
777
|
+
response.resizeWarnings = resized.warnings;
|
|
778
|
+
if (deletedFlows.length > 0)
|
|
779
|
+
response.deletedFlows = deletedFlows;
|
|
520
780
|
if (formWarnings.length > 0)
|
|
521
781
|
response.formWarnings = formWarnings;
|
|
522
782
|
if (flowWarnings.length > 0)
|
|
523
783
|
response.flowWarnings = flowWarnings;
|
|
524
784
|
if (layoutWarnings.length > 0)
|
|
525
785
|
response.layoutWarnings = layoutWarnings;
|
|
526
|
-
|
|
786
|
+
// G-23 micro-op ayağı: yama 200 döner ve `appliedPatch` op'un ŞEMAYA yazıldığını söyler, ama
|
|
787
|
+
// alanın gerçekten KAYDEDİLDİĞİNİ söylemez. Yamadan sonra şemayı bir kez okuyup op'un yazmak
|
|
788
|
+
// istediğiyle kayıtlı olanı karşılaştırıyoruz. Okuma başarısız olursa yama yine geçerlidir.
|
|
789
|
+
const contentOps = opRecords.filter(entry => entry.op === 'add_component' || entry.op === 'replace_prop');
|
|
790
|
+
let opDrops = [];
|
|
791
|
+
if (contentOps.length > 0) {
|
|
792
|
+
try {
|
|
793
|
+
const saved = await fetchPage(platform, project, environment, applicationId, pageId);
|
|
794
|
+
opDrops = schemaOpDrops(saved.schema, contentOps);
|
|
795
|
+
}
|
|
796
|
+
catch {
|
|
797
|
+
opDrops = [];
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
return withWriteDiff(withNormalization(response, [
|
|
527
801
|
...rootAddsNote(retargeted, PAGE_CONTAINER_ELEMENT_ID),
|
|
528
802
|
...componentDefaultsNote(normalized.components),
|
|
529
803
|
...validationNote(normalized.validations),
|
|
530
|
-
...
|
|
531
|
-
...heightTypeNote(catalog.heights),
|
|
804
|
+
...catalogNotes,
|
|
532
805
|
...replacePropNote(normalized.props)
|
|
533
|
-
]);
|
|
806
|
+
]), opDrops);
|
|
534
807
|
}));
|
|
535
808
|
server.registerTool('page_create', {
|
|
536
809
|
title: 'Sayfa oluştur',
|
|
537
|
-
description: "Yeni sayfa açar. Şemayı burada göndermek yerine boş bırakıp page_schema_patch micro-op'larıyla kurmak daha doğrudur (
|
|
810
|
+
description: "Yeni sayfa açar. Şemayı burada göndermek yerine boş bırakıp page_schema_patch micro-op'larıyla kurmak daha doğrudur (canlı canvas ve hedefli düzeltme orada). Şemayı burada gönderirseniz 2026-09-08'den beri AYNI doğrulama duvarından geçer: tanınmayan prop adı ve geçersiz enum değeri 422 ile reddedilir ve sayfa HİÇ açılmaz. roleIds için role_list, layoutId için layout_list kullanın. Yanıttaki schema VARSAYILAN OLARAK iskelettir (çözülmüş layout düğümleri dâhil id/type/children'a iner) — tam gövde için view:'full'; gönderdiğiniz bir alan kaydedilmediyse silentlyDropped söyler.",
|
|
538
811
|
inputSchema: {
|
|
539
812
|
project: projectParam,
|
|
540
813
|
environment: environmentParam,
|
|
@@ -542,30 +815,24 @@ export function registerPageTools(server, platform) {
|
|
|
542
815
|
name: z.string().min(1),
|
|
543
816
|
slug: z.string().min(1).describe("URL yolu, örn. 'orders' ya da 'orders/detail'"),
|
|
544
817
|
browserTitle: z.string().optional(),
|
|
818
|
+
browserTitleKey: z.string().optional().describe("Sekme başlığının çeviri anahtarı. Verilirse başlık çeviri sözlüğünden çözülür ve language-selector ile dil değişince sayfa yenilenmeden güncellenir; boşsa browserTitle olduğu gibi kullanılır. Anahtarlar için translation_list."),
|
|
545
819
|
layoutId: guid.optional(),
|
|
546
820
|
roleIds: z.array(guid).optional().describe('Sayfayı görebilecek roller; boş = herkese açık'),
|
|
547
821
|
middlewareIds: z.array(guid).optional(),
|
|
548
822
|
events: eventsParam.optional(),
|
|
549
|
-
schema: schemaParam.optional()
|
|
823
|
+
schema: schemaParam.optional(),
|
|
824
|
+
view: writeViewParam
|
|
550
825
|
},
|
|
551
826
|
annotations: writes
|
|
552
|
-
}, handle(config, async ({ project, environment, applicationId, name, slug, browserTitle, layoutId, roleIds, middlewareIds, events, schema }) => {
|
|
553
|
-
const badEvents = invalidScreenEventNames(events, 'PAGE-011');
|
|
554
|
-
if (badEvents.length > 0)
|
|
555
|
-
throw new PlatformApiError(`Geçersiz sayfa olayı (istek gönderilmedi): ${badEvents.join(' · ')}`, 400);
|
|
827
|
+
}, handle(config, async ({ project, environment, applicationId, name, slug, browserTitle, browserTitleKey, layoutId, roleIds, middlewareIds, events, schema, view }) => {
|
|
556
828
|
const container = ensurePageContainer(schema ?? []);
|
|
557
|
-
|
|
558
|
-
if (badWidgets.length > 0)
|
|
559
|
-
throw new PlatformApiError(`Geçersiz widget düğümü (istek gönderilmedi): ${badWidgets.join(' · ')}`, 400);
|
|
560
|
-
const nested = nestedRepeaterNodes(container.schema);
|
|
561
|
-
if (nested.length > 0)
|
|
562
|
-
throw new PlatformApiError(nestedRepeaterMessage(nested), 400);
|
|
829
|
+
assertScreenWriteGuards('page', { schema: container.schema, events });
|
|
563
830
|
const stringStyles = stringStyleNodes(container.schema);
|
|
564
831
|
const fixed = normalizeComponentDefaults(container.schema);
|
|
565
832
|
const reset = resetPageContainerPosition(container.schema);
|
|
566
|
-
const
|
|
567
|
-
? await
|
|
568
|
-
:
|
|
833
|
+
const catalogNotes = (schema ?? []).length > 0
|
|
834
|
+
? await screenCatalogNotes(platform, project, environment, applicationId, container.schema)
|
|
835
|
+
: [];
|
|
569
836
|
const moved = reorderSchemaBlocks(container.schema);
|
|
570
837
|
const gaps = (schema ?? []).length > 0 ? verticalGapWarnings(container.schema, 'PAGE-010 (uyarı)') : [];
|
|
571
838
|
const response = await platform.tenant('POST', project, `/pages/${applicationId}`, {
|
|
@@ -574,6 +841,7 @@ export function registerPageTools(server, platform) {
|
|
|
574
841
|
name,
|
|
575
842
|
slug,
|
|
576
843
|
browserTitle: browserTitle ?? '',
|
|
844
|
+
browserTitleKey: browserTitleKey ?? '',
|
|
577
845
|
layoutId: layoutId ?? null,
|
|
578
846
|
roleIds: roleIds ?? [],
|
|
579
847
|
middlewareIds: middlewareIds ?? [],
|
|
@@ -581,8 +849,20 @@ export function registerPageTools(server, platform) {
|
|
|
581
849
|
schema: container.schema
|
|
582
850
|
}
|
|
583
851
|
});
|
|
584
|
-
const
|
|
585
|
-
|
|
852
|
+
const saved = (response && typeof response === 'object' && !Array.isArray(response)
|
|
853
|
+
? (response.data ?? response)
|
|
854
|
+
: {});
|
|
855
|
+
// Kırpmadan ÖNCE doğrulama yolu (B-55): yanıt gövdesi iskelete inince "gönderdiğim prop
|
|
856
|
+
// kaydedildi mi" sorusunu gövde değil bu diff cevaplar. `layout_create`'in yaptığının sayfa
|
|
857
|
+
// ikizi; Prop sarmalı açılmazsa doğru kaydedilmiş her prop düşmüş görünür (G-28).
|
|
858
|
+
const drops = (schema ?? []).length > 0
|
|
859
|
+
? silentlyDropped({ schema: unwrapProps(container.schema) }, { schema: unwrapProps(saved.schema) })
|
|
860
|
+
: [];
|
|
861
|
+
if (view !== 'full' && Array.isArray(saved.schema)) {
|
|
862
|
+
saved.schema = saved.schema.map(node => summarize(node));
|
|
863
|
+
}
|
|
864
|
+
const normalized = withWriteDiff(withNormalization(response, [...container.fixes, ...reset, ...componentDefaultsNote(fixed), ...catalogNotes, ...reorderNote(moved)]), drops);
|
|
865
|
+
const createWarnings = [...gaps, ...stringStyleWarning(stringStyles), ...inertFlexPropWarning(inertFlexPropNodes(container.schema)), ...flexSizeWarnings(container.schema, 'PAGE-017 (uyarı)', 'PAGE-018 (uyarı)')];
|
|
586
866
|
if (createWarnings.length === 0 || !normalized || typeof normalized !== 'object')
|
|
587
867
|
return normalized;
|
|
588
868
|
return { ...normalized, layoutWarnings: createWarnings };
|
|
@@ -598,6 +878,7 @@ export function registerPageTools(server, platform) {
|
|
|
598
878
|
name: z.string().min(1).optional(),
|
|
599
879
|
slug: z.string().min(1).optional(),
|
|
600
880
|
browserTitle: z.string().optional(),
|
|
881
|
+
browserTitleKey: z.string().optional().describe("Sekme başlığının çeviri anahtarı. Verilirse başlık çeviri sözlüğünden çözülür ve language-selector ile dil değişince sayfa yenilenmeden güncellenir; boşsa browserTitle olduğu gibi kullanılır. Anahtarlar için translation_list."),
|
|
601
882
|
layoutId: guid.nullable().optional().describe('null: layout bağını kaldırır'),
|
|
602
883
|
roleIds: z.array(guid).optional(),
|
|
603
884
|
middlewareIds: z.array(guid).optional(),
|
|
@@ -607,31 +888,15 @@ export function registerPageTools(server, platform) {
|
|
|
607
888
|
annotations: updates
|
|
608
889
|
}, handle(config, async ({ project, environment, applicationId, pageId, ...changes }) => {
|
|
609
890
|
const overrides = Object.fromEntries(Object.entries(changes).filter(([, value]) => value !== undefined));
|
|
610
|
-
|
|
611
|
-
if (badEvents.length > 0)
|
|
612
|
-
throw new PlatformApiError(`Geçersiz sayfa olayı (istek gönderilmedi): ${badEvents.join(' · ')}`, 400);
|
|
613
|
-
const badWidgets = invalidWidgetTypeNodes(overrides.schema, 'PAGE-012');
|
|
614
|
-
if (badWidgets.length > 0)
|
|
615
|
-
throw new PlatformApiError(`Geçersiz widget düğümü (istek gönderilmedi): ${badWidgets.join(' · ')}`, 400);
|
|
616
|
-
const nestedUpdate = nestedRepeaterNodes(overrides.schema);
|
|
617
|
-
if (nestedUpdate.length > 0)
|
|
618
|
-
throw new PlatformApiError(nestedRepeaterMessage(nestedUpdate), 400);
|
|
891
|
+
assertScreenWriteGuards('page', { schema: overrides.schema, events: overrides.events });
|
|
619
892
|
const current = await fetchHost(platform, 'page', project, environment, applicationId, pageId);
|
|
620
|
-
const badSchemaNames = invalidComponentNames(overrides.schema);
|
|
621
|
-
if (badSchemaNames.length > 0) {
|
|
622
|
-
throw new PlatformApiError(`PAGE-009 — komponent adı JS tanımlayıcısı değil (istek gönderilmedi): ${badSchemaNames.join(' · ')}. `
|
|
623
|
-
+ 'Runtime her komponent için `const <ad> = Components["<ad>"]` üretir; geçersiz bir ad sayfadaki TÜM ifadeleri öldürür. '
|
|
624
|
-
+ 'name = elementId yazın.', 400);
|
|
625
|
-
}
|
|
626
893
|
const fixed = normalizeComponentDefaults(overrides.schema);
|
|
627
894
|
const reset = overrides.schema === undefined ? [] : resetPageContainerPosition(overrides.schema);
|
|
628
|
-
const
|
|
629
|
-
? { clamped: [], heights: [] }
|
|
630
|
-
: await applyCatalogLayoutRules(platform, project, environment, applicationId, overrides.schema);
|
|
895
|
+
const catalogNotes = await screenCatalogNotes(platform, project, environment, applicationId, overrides.schema);
|
|
631
896
|
const moved = overrides.schema === undefined ? [] : reorderSchemaBlocks(overrides.schema);
|
|
632
897
|
const gaps = overrides.schema === undefined
|
|
633
898
|
? []
|
|
634
|
-
: [...verticalGapWarnings(overrides.schema, 'PAGE-010 (uyarı)'), ...inertFlexPropWarning(inertFlexPropNodes(overrides.schema))];
|
|
899
|
+
: [...verticalGapWarnings(overrides.schema, 'PAGE-010 (uyarı)'), ...inertFlexPropWarning(inertFlexPropNodes(overrides.schema)), ...flexSizeWarnings(overrides.schema, 'PAGE-017 (uyarı)', 'PAGE-018 (uyarı)')];
|
|
635
900
|
const body = pageUpdateBody(current, overrides);
|
|
636
901
|
await platform.tenant('PUT', project, `/pages/${applicationId}/${pageId}`, { environment, body });
|
|
637
902
|
const summary = {
|
|
@@ -641,7 +906,7 @@ export function registerPageTools(server, platform) {
|
|
|
641
906
|
};
|
|
642
907
|
if (gaps.length > 0)
|
|
643
908
|
summary.layoutWarnings = gaps;
|
|
644
|
-
return withNormalization(summary, [...reset, ...componentDefaultsNote(fixed), ...
|
|
909
|
+
return withNormalization(summary, [...reset, ...componentDefaultsNote(fixed), ...catalogNotes, ...reorderNote(moved)]);
|
|
645
910
|
}));
|
|
646
911
|
server.registerTool('page_clone', {
|
|
647
912
|
title: 'Sayfa klonla',
|