@appaflytech/zeyrek-ai-mcp 0.0.7 → 0.0.9
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/README.md +27 -21
- package/dist/config.js +2 -0
- package/dist/knowledge/backendConstraints.js +3 -0
- package/dist/knowledge/componentCatalog.js +172 -33
- package/dist/knowledge/componentEvents.js +35 -4
- package/dist/knowledge/errorPath.js +1 -1
- package/dist/knowledge/normalize.js +474 -23
- package/dist/knowledge/propTypes.js +5 -4
- package/dist/knowledge/rules.js +113 -6
- package/dist/knowledge/styleResolve.js +52 -5
- package/dist/knowledge/themeResolve.js +936 -0
- package/dist/knowledge/themeRules.js +221 -0
- package/dist/knowledge/themeSchema.js +335 -0
- package/dist/platform/domSummary.js +144 -0
- package/dist/platform/http.js +10 -2
- package/dist/platform/screenshot.js +492 -29
- package/dist/platform/screenshotMemory.js +55 -0
- package/dist/platform/strictArguments.js +32 -0
- package/dist/platform/writeDiff.js +6 -3
- package/dist/resources/examples/layout-app-shell.json +1 -1
- package/dist/resources/examples/page-sign-in.json +1 -1
- package/dist/resources/examples/theme-brand.json +162 -0
- package/dist/resources/guides/component.md +8 -2
- package/dist/resources/guides/expressions.md +6 -0
- package/dist/resources/guides/getting-started.md +6 -0
- package/dist/resources/guides/layout.md +36 -26
- package/dist/resources/guides/microservice.md +160 -6
- package/dist/resources/guides/page.md +165 -59
- package/dist/resources/guides/theme.md +210 -0
- package/dist/resources/guides/translation.md +13 -10
- package/dist/resources/guides/workflow.md +141 -7
- package/dist/resources/schema-fingerprint.json +2 -2
- package/dist/resources/schemas/CreateMicroserviceCommand.json +4 -1
- package/dist/resources/schemas/CreateWorkflowCommand.json +4 -1
- package/dist/resources/schemas/PreviewMicroserviceCommand.json +4 -1
- package/dist/resources/schemas/PreviewWorkflowCommand.json +4 -1
- package/dist/resources/schemas/StepDto.json +4 -1
- package/dist/resources/schemas/UpdateMicroserviceCommand.json +4 -1
- package/dist/resources/schemas/UpdateWorkflowCommand.json +4 -1
- package/dist/server.js +52 -42
- package/dist/tools/components.js +8 -0
- package/dist/tools/designSchemaPatch.js +152 -0
- package/dist/tools/flows.js +24 -21
- package/dist/tools/layouts.js +8 -8
- package/dist/tools/microservices.js +74 -10
- package/dist/tools/pages.js +275 -332
- package/dist/tools/projects.js +66 -9
- package/dist/tools/queries.js +72 -6
- package/dist/tools/schemaOps.js +280 -0
- package/dist/tools/screenWrite.js +2 -1
- package/dist/tools/screens.js +0 -7
- package/dist/tools/themes.js +163 -0
- package/dist/tools/workflows.js +10 -6
- package/package.json +1 -1
package/dist/tools/pages.js
CHANGED
|
@@ -1,21 +1,24 @@
|
|
|
1
1
|
import { guid, projectParam, environmentParam, pagingParams, readOnly, writes, updates, destructive, microOps, isBranchName } 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,
|
|
2
|
+
import { orphanedFlowWarnings, orphanedFlowPlan, orphanedFlowMessage, staleSetterWarnings, lostSetterTargets, normalizeComponentDefaults, normalizeSchemaOps, schemaOpValidationErrors, codePropTemplateErrors, validationNote, componentDefaultsNote, replacePropNote, ensurePageContainer, retargetRootAdds, rootAddsNote, flexSizeWarnings, spacingLayerWarnings, reorderSchemaBlocks, reorderNote, nestedRepeaterNodes, nestedRepeaterMessage, stringStyleNodes, stringStyleWarning, inertFlexPropNodes, inertFlexPropWarning, FLEX_LAYOUT_PROP_NAMES, resetPageContainerPosition, pageContainerPositionWarning, verticalGapWarnings, flexIndexOrder, flexIndexOrderNote, withNormalization, parseVariableEntries, PAGE_CONTAINER_ELEMENT_ID } from '../knowledge/normalize.js';
|
|
3
3
|
import { functionSignature } from '../knowledge/functionGuards.js';
|
|
4
|
-
import { fetchHost, pageUpdateBody, schemaParam, eventsParam, unwrap, modeParam, summarize,
|
|
4
|
+
import { fetchHost, pageUpdateBody, schemaParam, eventsParam, unwrap, modeParam, summarize, findNode, flowPath } from './screens.js';
|
|
5
5
|
import { fetchComponentEventNames } from './components.js';
|
|
6
6
|
import { assertScreenWriteGuards, screenCatalogNotes } from './screenWrite.js';
|
|
7
|
+
import { opsParam, writeViewParam, outlineWriteResponse, unknownEventOps, collectTypes, simulateLayoutOps, planEventRemovals, resizeServerOps, planResizes, planPropTypes, rejectedOpsMessage } from './schemaOps.js';
|
|
7
8
|
import { formBindingWarnings, formSubmitButtonWarnings } from '../knowledge/coverage.js';
|
|
8
9
|
import { schemaOpDrops, silentlyDropped, unwrapProps, withWriteDiff } from '../platform/writeDiff.js';
|
|
9
10
|
import { PlatformApiError } from '../platform/errors.js';
|
|
10
11
|
import { handle, toolResult, toolError } from '../platform/responses.js';
|
|
11
12
|
import { StudioScreenshotter } from '../platform/screenshot.js';
|
|
13
|
+
import { diffSummaries, summaryLines, summaryStats } from '../platform/domSummary.js';
|
|
14
|
+
import { captureKey, imageHash, sharedCaptureMemory } from '../platform/screenshotMemory.js';
|
|
12
15
|
import { summarizeLayout } from '../knowledge/layoutSummary.js';
|
|
13
16
|
import { normalizeLanguageCode } from '../knowledge/languageCodes.js';
|
|
14
17
|
import { readProjectLanguages } from '../knowledge/languages.js';
|
|
15
18
|
import { resolveIdentityToken } from '../platform/tokenVault.js';
|
|
16
19
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
17
20
|
import { dirname, resolve } from 'node:path';
|
|
18
|
-
import {
|
|
21
|
+
import { setPropTypeErrors } from '../knowledge/propTypes.js';
|
|
19
22
|
import { z } from 'zod';
|
|
20
23
|
/**
|
|
21
24
|
* Runtime paketindeki `appFunctions`/`pageFunctions` (UiFunctionRuntimeDto: id, name, parameters,
|
|
@@ -36,261 +39,35 @@ function runtimeFunctions(value) {
|
|
|
36
39
|
}));
|
|
37
40
|
}
|
|
38
41
|
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* durur. Bayrağı bilmeyen ajan pahalı olanı ödemesin diye kırpma OPT-OUT'tur (aynı karar:
|
|
42
|
-
* `DS-004`, `datasource_configuration_options`).
|
|
42
|
+
* G-10: `index` taşıyan add/move op'u varsa sayfa okunur ve `flexIndexOrder` flex kapta görünen
|
|
43
|
+
* sırayı konumlara çevirir. Okuma başarısız olursa op'lar olduğu gibi gider — bugünkü davranış.
|
|
43
44
|
*/
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
*/
|
|
48
|
-
function outlineWriteResponse(response) {
|
|
49
|
-
const inner = response.data;
|
|
50
|
-
const container = (inner && typeof inner === 'object' && !Array.isArray(inner) ? inner : response);
|
|
51
|
-
if (Array.isArray(container.appliedPatch))
|
|
52
|
-
container.appliedPatch = outlineAppliedPatch(container.appliedPatch);
|
|
53
|
-
}
|
|
54
|
-
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.");
|
|
55
|
-
/**
|
|
56
|
-
* PAGE-008 — `set_event` uydurma bir olay adıyla 200 döner ve bağ SESSİZCE ölü kalır: motor olayı
|
|
57
|
-
* komponentin yaydığı adla eşleştirir, eşleşme yoksa akış hiç çalışmaz. Kontrol yalnız hedefin
|
|
58
|
-
* tipini ve o tipin katalogdaki olay listesini pozitif olarak çözebildiğinde konuşur (layout/widget
|
|
59
|
-
* içindeki hedefler ve katalogda olay tanımlamayan tipler doğrulanmadan geçer).
|
|
60
|
-
*/
|
|
61
|
-
function unknownEventOps(ops, typesByElementId, eventsBySlug) {
|
|
62
|
-
return ops
|
|
63
|
-
.map(op => op)
|
|
64
|
-
.filter(op => op.op === 'set_event')
|
|
65
|
-
.flatMap(op => {
|
|
66
|
-
const target = String(op.target ?? '');
|
|
67
|
-
const event = String(op.event ?? '');
|
|
68
|
-
const type = typesByElementId.get(target);
|
|
69
|
-
const declared = type ? eventsBySlug.get(type) : undefined;
|
|
70
|
-
if (!event || !declared || declared.includes(event))
|
|
71
|
-
return [];
|
|
72
|
-
return [`'${target}' (${type}) komponenti '${event}' diye bir olay YAYMAZ. Yaydıkları: ${declared.join(', ')}. Platform bu op'u 200 ile kabul eder ama bağ sessizce ölü kalır — akış hiçbir zaman çalışmaz. Doğru adı component_get(slug:'${type}') çıktısındaki events listesinden alın.`];
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
function collectTypes(nodes, into) {
|
|
76
|
-
for (const node of nodes ?? []) {
|
|
77
|
-
if (typeof node.elementId === 'string' && typeof node.type === 'string')
|
|
78
|
-
into.set(node.elementId, node.type);
|
|
79
|
-
collectTypes(node.children, into);
|
|
45
|
+
async function orderByIndex(ops, readPage) {
|
|
46
|
+
if (!ops.some(entry => (entry.op === 'add_component' || entry.op === 'move_component') && typeof entry.index === 'number')) {
|
|
47
|
+
return { ops, moved: [], warnings: [] };
|
|
80
48
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
for (let index = 0; index < nodes.length; index++) {
|
|
84
|
-
if (nodes[index].elementId === target)
|
|
85
|
-
return nodes.splice(index, 1)[0];
|
|
86
|
-
const found = removeNodeFromTree(nodes[index].children ?? [], target);
|
|
87
|
-
if (found)
|
|
88
|
-
return found;
|
|
49
|
+
try {
|
|
50
|
+
return flexIndexOrder((await readPage()).schema, ops);
|
|
89
51
|
}
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
function setDesktopPosition(node, position) {
|
|
93
|
-
const blocks = Array.isArray(node.blocks) ? node.blocks : [];
|
|
94
|
-
const block = blocks.find(entry => Number(entry?.breakpoint) === 1024);
|
|
95
|
-
if (block)
|
|
96
|
-
block.position = { x: Number(position.x ?? 0), y: Number(position.y ?? 0) };
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* PAGE-010 boşluk analizi patch'in SONUCUNU görmek zorunda: tek başına op'lardaki y değerleri,
|
|
100
|
-
* kapsayıcının mevcut çocukları bilinmeden anlamsız. Op'lar okunan şemanın kopyasına uygulanır
|
|
101
|
-
* (add: parent'ın children'ına ekle — kökler retargetRootAdds ile page_container'a gittiği için
|
|
102
|
-
* burada da öyle çözülür; move: düğümü taşı ve verilen position'ı desktop block'una yaz) ve analiz
|
|
103
|
-
* yalnız dokunulan kapsayıcılarda koşar.
|
|
104
|
-
*/
|
|
105
|
-
function simulateLayoutOps(schema, ops) {
|
|
106
|
-
const touched = new Set();
|
|
107
|
-
const attach = (node, requestedParent) => {
|
|
108
|
-
const parentId = typeof requestedParent === 'string' && requestedParent !== '' && requestedParent !== 'root'
|
|
109
|
-
? requestedParent
|
|
110
|
-
: PAGE_CONTAINER_ELEMENT_ID;
|
|
111
|
-
const parent = findNode(schema, parentId);
|
|
112
|
-
if (parent) {
|
|
113
|
-
parent.children = [...(parent.children ?? []), node];
|
|
114
|
-
touched.add(String(parent.elementId ?? parentId));
|
|
115
|
-
}
|
|
116
|
-
else {
|
|
117
|
-
schema.push(node);
|
|
118
|
-
touched.add('kök');
|
|
119
|
-
}
|
|
120
|
-
};
|
|
121
|
-
for (const op of ops) {
|
|
122
|
-
if (op.op === 'add_component' && op.component && typeof op.component === 'object') {
|
|
123
|
-
attach(JSON.parse(JSON.stringify(op.component)), op.parent);
|
|
124
|
-
}
|
|
125
|
-
// Düzen prop'ları (layout/flex grubu) simülasyona girer: PAGE-010 flex kapsayıcının içini
|
|
126
|
-
// analiz etmez, PAGE-015 ise op'tan SONRAKİ layout değerine bakar — ikisi de op uygulanmadan
|
|
127
|
-
// doğru karar veremez.
|
|
128
|
-
if (op.op === 'replace_prop' && FLEX_LAYOUT_PROP_NAMES.has(String(op.prop ?? ''))) {
|
|
129
|
-
const target = findNode(schema, String(op.target ?? ''));
|
|
130
|
-
if (target)
|
|
131
|
-
target.props = { ...(target.props ?? {}), [String(op.prop)]: { type: 'static', ref: null, data: op.value } };
|
|
132
|
-
}
|
|
133
|
-
if (op.op === 'move_component') {
|
|
134
|
-
const moved = removeNodeFromTree(schema, String(op.target ?? ''));
|
|
135
|
-
if (!moved)
|
|
136
|
-
continue;
|
|
137
|
-
if (op.position && typeof op.position === 'object')
|
|
138
|
-
setDesktopPosition(moved, op.position);
|
|
139
|
-
attach(moved, op.parent);
|
|
140
|
-
}
|
|
52
|
+
catch {
|
|
53
|
+
return { ops, moved: [], warnings: [] };
|
|
141
54
|
}
|
|
142
|
-
return touched;
|
|
143
55
|
}
|
|
144
56
|
async function fetchPage(platform, project, environment, applicationId, pageId) {
|
|
145
57
|
const response = await platform.tenant('GET', project, `/pages/${applicationId}/${pageId}`, { environment });
|
|
146
58
|
return response?.data ?? {};
|
|
147
59
|
}
|
|
148
60
|
/**
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
* `target: 'page'` sayfa düzeyi olayları hedefler.
|
|
154
|
-
*/
|
|
155
|
-
async function applyEventRemovals(platform, project, environment, applicationId, pageId, page, ops) {
|
|
156
|
-
const schema = JSON.parse(JSON.stringify(page.schema ?? []));
|
|
157
|
-
const pageEvents = Array.isArray(page.events) ? [...page.events] : [];
|
|
158
|
-
const removed = [];
|
|
159
|
-
let pageEventsTouched = false;
|
|
160
|
-
for (const op of ops) {
|
|
161
|
-
const target = String(op.target ?? '');
|
|
162
|
-
const event = String(op.event ?? '');
|
|
163
|
-
if (!target || !event)
|
|
164
|
-
throw new PlatformApiError("remove_event için 'target' ve 'event' zorunludur.", 400);
|
|
165
|
-
if (target === 'page') {
|
|
166
|
-
const before = pageEvents.length;
|
|
167
|
-
const kept = pageEvents.filter(entry => String(entry?.name ?? '') !== event);
|
|
168
|
-
if (kept.length !== before) {
|
|
169
|
-
pageEvents.length = 0;
|
|
170
|
-
pageEvents.push(...kept);
|
|
171
|
-
pageEventsTouched = true;
|
|
172
|
-
removed.push(`page.${event}`);
|
|
173
|
-
}
|
|
174
|
-
continue;
|
|
175
|
-
}
|
|
176
|
-
const node = findNode(schema, target);
|
|
177
|
-
if (!node)
|
|
178
|
-
throw new PlatformApiError(`remove_event hedefi bulunamadı: '${target}'. page_schema_get ile elementId'leri listeleyin.`, 400);
|
|
179
|
-
const events = Array.isArray(node.events) ? node.events : [];
|
|
180
|
-
const kept = events.filter(entry => String(entry?.name ?? '') !== event);
|
|
181
|
-
if (kept.length !== events.length)
|
|
182
|
-
removed.push(`${target}.${event}`);
|
|
183
|
-
node.events = kept;
|
|
184
|
-
}
|
|
185
|
-
if (removed.length === 0)
|
|
186
|
-
return [];
|
|
187
|
-
const body = pageUpdateBody(page, pageEventsTouched ? { schema, events: pageEvents } : { schema });
|
|
188
|
-
await platform.tenant('PUT', project, `/pages/${applicationId}/${pageId}`, { environment, body });
|
|
189
|
-
return removed;
|
|
190
|
-
}
|
|
191
|
-
function findParentNode(nodes, target) {
|
|
192
|
-
for (const node of nodes) {
|
|
193
|
-
if ((node.children ?? []).some(child => child.elementId === target))
|
|
194
|
-
return node;
|
|
195
|
-
const deeper = findParentNode(node.children ?? [], target);
|
|
196
|
-
if (deeper)
|
|
197
|
-
return deeper;
|
|
198
|
-
}
|
|
199
|
-
return undefined;
|
|
200
|
-
}
|
|
201
|
-
/**
|
|
202
|
-
* `resize_component` gateway emülasyonudur: platformun micro-op sözlüğünde boyut YOKTUR
|
|
203
|
-
* (`move_component` yalnız `position` uygular, gövdeye konan `size` 200 döner ve sessizce düşer —
|
|
204
|
-
* ölçüldü 09-04). Bu yüzden kurulmuş bir sayfada bir kartın yüksekliğini değiştirmenin tek yolu
|
|
205
|
-
* düğümü `remove_component` + `add_component` ile yeniden kurmaktı; o da ona yazan akış
|
|
206
|
-
* setter'larını sessizce öldürüyordu (FLOW-018). Op şemayı okur, hedefin boyutunu değiştirir ve
|
|
207
|
-
* sayfayı tam gövdeyle geri yazar — `remove_event` ile aynı desen.
|
|
208
|
-
*
|
|
209
|
-
* Izgarada boyut `blocks[].size`, flex'te `appearance` alanlarındadır; ikisi de kabul edilir ve
|
|
210
|
-
* ebeveynin düzeni okunanı etkisiz kılıyorsa uyarı üretilir (PAGE-015 ile aynı yaklaşım: reddetme,
|
|
211
|
-
* ama sessiz de kalma).
|
|
212
|
-
*/
|
|
213
|
-
/**
|
|
214
|
-
* G-25 — `states` bir prop değil düğüm alanı: `replace_prop` yazamıyor, `move_component` yalnız
|
|
215
|
-
* konum uyguluyor ve `resize_component` (G-14) boyutla sınırlıydı. Kurulmuş bir düğümün
|
|
216
|
-
* `isWrapper` bayrağını değiştirmenin tek yolu düğümü silip yeniden kurmaktı; 09-05 koşusunda
|
|
217
|
-
* bunun bedeli 20 setter'ın sessizce kopmasıydı (G-21). Anahtar kümesi beyaz listeyle sınırlı:
|
|
218
|
-
* `states` açık uçlu bir sözlük ve uydurma bayrak sessizce kaydedilirdi.
|
|
61
|
+
* Platformun sayfa micro-op sözlüğü (`PageSchemaMicroOpApplier.KnownOps`). Gateway'in sunucuya
|
|
62
|
+
* ilettiği op'lar yalnız bunlardır; `test/page-micro-op-parity.test.ts` listeyi backend ve
|
|
63
|
+
* `@ng-apps/screen-builder` ile karşılaştırır. `resize_component` gateway adıdır ve sunucuya
|
|
64
|
+
* `set_block` + `set_appearance` + `set_states` olarak çevrilir.
|
|
219
65
|
*/
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
for (const op of ops) {
|
|
226
|
-
const target = String(op.target ?? '');
|
|
227
|
-
const size = op.size && typeof op.size === 'object' ? op.size : undefined;
|
|
228
|
-
const appearance = op.appearance && typeof op.appearance === 'object' ? op.appearance : undefined;
|
|
229
|
-
const states = op.states && typeof op.states === 'object' ? op.states : undefined;
|
|
230
|
-
const breakpoint = Number(op.breakpoint ?? 1024);
|
|
231
|
-
if (!target)
|
|
232
|
-
throw new PlatformApiError("resize_component için 'target' zorunludur.", 400);
|
|
233
|
-
if (!size && !appearance && !states) {
|
|
234
|
-
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);
|
|
235
|
-
}
|
|
236
|
-
if (states) {
|
|
237
|
-
const unknownStates = Object.keys(states).filter(key => !WRITABLE_STATES.has(key));
|
|
238
|
-
if (unknownStates.length > 0) {
|
|
239
|
-
throw new PlatformApiError(`resize_component '${target}': bilinmeyen states anahtarı (${unknownStates.join(', ')}). Yazılabilir bayraklar: ${[...WRITABLE_STATES].join(', ')}.`, 400);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
const node = findNode(schema, target);
|
|
243
|
-
if (!node)
|
|
244
|
-
throw new PlatformApiError(`resize_component hedefi bulunamadı: '${target}'. page_schema_get ile elementId'leri listeleyin.`, 400);
|
|
245
|
-
const parent = findParentNode(schema, target);
|
|
246
|
-
const inFlex = parent !== undefined && isFlexContainer(parent);
|
|
247
|
-
if (size) {
|
|
248
|
-
const blocks = Array.isArray(node.blocks) ? node.blocks : [];
|
|
249
|
-
const block = blocks.find(entry => Number(entry?.breakpoint) === breakpoint);
|
|
250
|
-
if (!block) {
|
|
251
|
-
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);
|
|
252
|
-
}
|
|
253
|
-
const current = (block.size ?? {});
|
|
254
|
-
block.size = { w: Number(size.w ?? current.w ?? 0), h: Number(size.h ?? current.h ?? 0) };
|
|
255
|
-
applied.push(`${target}.size=${JSON.stringify(block.size)}`);
|
|
256
|
-
if (inFlex) {
|
|
257
|
-
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.`);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
if (states) {
|
|
261
|
-
node.states = { ...(node.states ?? {}), ...states };
|
|
262
|
-
applied.push(`${target}.states=${JSON.stringify(states)}`);
|
|
263
|
-
if (states.isWrapper === true) {
|
|
264
|
-
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.`);
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
if (appearance) {
|
|
268
|
-
node.appearance = { ...(node.appearance ?? {}), ...appearance };
|
|
269
|
-
applied.push(`${target}.appearance=${JSON.stringify(appearance)}`);
|
|
270
|
-
if (!inFlex && parent !== undefined) {
|
|
271
|
-
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.`);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
if (applied.length === 0)
|
|
276
|
-
return { applied, warnings };
|
|
277
|
-
const body = pageUpdateBody(page, { schema });
|
|
278
|
-
await platform.tenant('PUT', project, `/pages/${applicationId}/${pageId}`, { environment, body });
|
|
279
|
-
return { applied, warnings };
|
|
280
|
-
}
|
|
281
|
-
async function applyPropTypes(platform, project, environment, applicationId, pageId, page, ops) {
|
|
282
|
-
const schema = JSON.parse(JSON.stringify(page.schema ?? []));
|
|
283
|
-
const { applied, missingTargets } = applyPropTypesToSchema(schema, ops);
|
|
284
|
-
if (missingTargets.length > 0) {
|
|
285
|
-
throw new PlatformApiError(`set_prop_type hedefi bulunamadı: '${missingTargets[0]}'. page_schema_get ile elementId'leri listeleyin.`, 400);
|
|
286
|
-
}
|
|
287
|
-
if (applied.length === 0)
|
|
288
|
-
return [];
|
|
289
|
-
const body = pageUpdateBody(page, { schema });
|
|
290
|
-
await platform.tenant('PUT', project, `/pages/${applicationId}/${pageId}`, { environment, body });
|
|
291
|
-
return applied;
|
|
292
|
-
}
|
|
293
|
-
export function registerPageTools(server, platform, injectedScreenshotter) {
|
|
66
|
+
export const SERVER_PAGE_OPS = [
|
|
67
|
+
'replace_prop', 'set_binding', 'set_prop_type', 'add_component', 'remove_component', 'move_component', 'reorder', 'set_block',
|
|
68
|
+
'set_appearance', 'set_states', 'set_event', 'remove_event', 'set_validations', 'set_state_binding', 'remove_state_binding', 'batch'
|
|
69
|
+
];
|
|
70
|
+
export function registerPageTools(server, platform, injectedScreenshotter, injectedMemory) {
|
|
294
71
|
const config = platform.config;
|
|
295
72
|
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.");
|
|
296
73
|
/** Sayfa ve uç araçları aynı kimlik sözleşmesini paylaşır (G-20); çözüm tokenVault'ta yaşar. */
|
|
@@ -401,12 +178,23 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
|
|
|
401
178
|
appFunctions: runtimeFunctions(body.appFunctions),
|
|
402
179
|
pageFunctions: runtimeFunctions(body.pageFunctions),
|
|
403
180
|
hasTheme: Boolean(body.project?.theme),
|
|
181
|
+
// Tema ADLARI burada durur: bir düğüme $preset yazmadan önce hangi adların geçerli olduğunu
|
|
182
|
+
// bilmek gerekir ve bunun için ayrı bir çağrı atmak gereksiz (tam kayıt için theme_get).
|
|
183
|
+
...(theme
|
|
184
|
+
? {
|
|
185
|
+
theme: {
|
|
186
|
+
presets: Object.keys(theme.presets ?? {}),
|
|
187
|
+
componentStyles: Object.keys(theme.componentStyles ?? {})
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
: {}),
|
|
404
191
|
note: resolveStyles
|
|
405
|
-
? "schema, layout'taki slot'un İÇİNE yerleşerek çizilir (Kural 1f). resolvedStyle = componentStyles → preset → düğüm stili kaskadı (aibox'ta tipli grupların derlemesi TABAN katmandır); styleLayers hangi katmanların konuştuğunu söyler."
|
|
192
|
+
? "schema, layout'taki slot'un İÇİNE yerleşerek çizilir (Kural 1f). resolvedStyle = componentStyles → preset → düğüm stili kaskadı (aibox'ta tipli grupların derlemesi TABAN katmandır); styleLayers hangi katmanların konuştuğunu söyler — styleLayers.componentPreset ise CSS DEĞİL, düğümün hangi bileşen preseti versiyonundan kurulduğunun işaretidir."
|
|
406
193
|
: "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."
|
|
407
194
|
};
|
|
408
195
|
}));
|
|
409
196
|
const screenshotter = injectedScreenshotter ?? new StudioScreenshotter(config);
|
|
197
|
+
const captureMemory = injectedMemory ?? sharedCaptureMemory;
|
|
410
198
|
const maxInlinePngBytes = 5 * 1024 * 1024;
|
|
411
199
|
server.registerTool('page_screenshot', {
|
|
412
200
|
title: 'Sayfanın ekran görüntüsünü al',
|
|
@@ -429,21 +217,42 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
|
|
|
429
217
|
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."),
|
|
430
218
|
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."),
|
|
431
219
|
actions: z.array(z.object({
|
|
432
|
-
action: z.enum(['set', 'click', 'wait']),
|
|
433
|
-
target: z.string().min(1).optional().describe("Hedefin elementId'si (page_schema_get); runtime düğümü data-element-id ile bulunur"),
|
|
434
|
-
|
|
220
|
+
action: z.enum(['set', 'click', 'wait', 'drag', 'tab', 'select', 'submit']),
|
|
221
|
+
target: z.string().min(1).optional().describe("Hedefin elementId'si (page_schema_get); runtime düğümü data-element-id ile bulunur. 'drag' için sürüklenen öğe (ör. 'kb_board__card-42', 'cal_plan__event-7'); 'tab' için sekme GRUBUNUN kendisi ('tabs_settings'), 'select' için açılır kutu, 'submit' için form komponenti"),
|
|
222
|
+
to: z.string().min(1).optional().describe("Yalnız 'drag': bırakma hedefinin elementId'si — kanban kolonu '<elementId>__col-<value>', takvim günü '<elementId>__day-<yyyy-MM-dd>', tüm gün hücresi '<elementId>__allday-<yyyy-MM-dd>', saat dilimi '<elementId>__slot-<yyyy-MM-dd>T<HH>'"),
|
|
223
|
+
value: z.unknown().optional().describe("'set' için yazılacak değer. Tarih kutularına ISO yazın ('2026-08-01'); çok alanlı tarih aralığında kök DEĞİL kutu hedeflenir: '<elementId>__start' / '<elementId>__end'. 'tab' için sekmenin ANAHTARI ('iot') ya da görünen metni; 'select' için seçeneğin value'su ya da görünen metni"),
|
|
435
224
|
ms: z.number().int().min(0).max(15000).optional().describe("'wait' için bekleme süresi")
|
|
436
|
-
})).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)."),
|
|
225
|
+
})).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'}, {action:'tab', target:'tabs_settings', value:'iot'}, {action:'select', target:'dd_city', value:'34'}, {action:'submit', target:'frm_note'}, {action:'drag', target:'kb_board__card-42', to:'kb_board__col-Assigned'}]. 'tab' SEKME SEÇER: sekme grubunun köküne 'click' göndermek DOM sırasındaki İLK sekmeye gider ve ikinci sekme hiç açılamıyordu — tablist köküne click artık reddedilir ve bu fiile yönlendirir. Eşleme önce '<elementId>__tab-<sekme anahtarı>' kimliğiyle, o yoksa görünen metinle yapılır; actionsReport.matchedBy hangisi olduğunu, actionsReport.activeTab adımdan SONRA hangi sekmenin açık olduğunu söyler. Etkin olmayan sekmenin içeriği DOM'da YOKTUR: o sekmedeki bir düğüme yazmadan önce 'tab' adımı gerekir. 'select' açılır kutudan seçer — kutu native alan değildir, değeri yazmak hiçbir şey yapmaz, seçeneğe tıklanması gerekir; liste 3 sn'ye kadar YOKLANIR (sorgudan beslenen kutuda seçenekler geç basılır) ve actionsReport.optionsSeen + waitedMs neyin görüldüğünü söyler. 'submit' formu gönder butonu OLMADAN gönderir (form.requestSubmit); actionsReport.submitVia hangi yolun kullanıldığını yazar. 'drag' HTML5 sürükle-bırak olaylarını (dragstart→dragover→drop→dragend) hedefin merkezinde atar; kanban cardMove ve takvim eventMove bununla tetiklenir. 'set' native setter kullanır ve input+change olaylarını bubbles ile atar (düz DOM ataması framework'ün duyduğu olayı üretmez); ardından kök düğümün runtime prop'larını yeniden okur: actionsReport.propWritten değerin prop'a ULAŞIP ulaşmadığıdır ve false ise applied da false döner — DOM'a yazılmış ama bileşenin duymadığı değer 'uygulandı' sayılmaz (G-14). 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)."),
|
|
437
226
|
language: z.string().min(1).optional().describe("Kareyi BU DİLDE çeker: adrese '?lang=<kod>' eklenir ve runtime dili oradan seçer (öncelik: ?lang → tarayıcıda kayıtlı seçim → tarayıcı dilleri → projenin varsayılanı). Seçim hem çeviriye bağlı prop'ları hem çok dilli alanların satır değerlerini kapsar, yani ikinci dilin karesi için sayfaya dil değiştirici KOYMAK GEREKMEZ — referans tasarımı bozulmadan ikinci dil kanıtlanır. ISO 639-1 iki harfli kod ('tr', 'en'); bölgeli kod ('tr-TR') indirgenir ve indirgeme yanıtta bildirilir. Kod projenin languages listesinde yoksa çağrı REDDEDİLİR: runtime tanımadığı kodu yok sayıp kareyi varsayılan dilde çizerdi ve 'çevrildi' sanılan yanlış bir kare raporlanırdı."),
|
|
438
227
|
colorScheme: z.enum(['light', 'dark']).optional().describe("Kareyi açık ya da koyu temada çeker. Ziyaretçi tema tercihi çerezde değil localStorage'da yaşar ve tema store'u onu sayfa açılırken bir kez okur; gateway tercihi sayfa script'lerinden ÖNCE eker, bu yüzden sayfada 'theme-toggle' komponenti OLMASA DA koyu tema karesi alınabilir. Verilmezse projenin kendi modu çizilir."),
|
|
228
|
+
format: z.enum(['png', 'jpeg']).optional().describe("Kare biçimi; varsayılan 'png'. Doğrulama karesi için 'jpeg' + quality 60 bağlamı küçültür (ölçüldü 2026-09-16, aynı sayfa: scale 1'de png 8639 B / jpeg60 8691 B — kazanç yok; scale 0.5'te png 3603 B / jpeg60 3035 B). Asıl kaldıraç ÖLÇEKTİR; renk ve kenar keskinliğinin kendisi inceleniyorsa png'de kalın."),
|
|
229
|
+
quality: z.number().int().min(1).max(100).optional().describe("Yalnız format:'jpeg' ile: 1-100, doğrulama için 60 yeter. format 'png' iken verilirse çağrı reddedilir — PNG kayıpsızdır, kalite ayarı yoktur."),
|
|
230
|
+
scale: z.number().min(0.25).max(1).optional().describe("Karenin RASTER ölçeği (deviceScaleFactor); varsayılan 1. 0.5 verildiğinde sayfa yine 1280 CSS pikseli genişliğinde ÇİZİLİR ama kare 640 piksel çıkar: yerleşim, kırılım noktaları ve hideOn davranışı değişmez, yalnız görüntü küçülür. Dar ekran denemek için scale değil viewport kullanın. Yanıttaki imageSize ölçeğin gerçekten uygulandığını sayıyla gösterir."),
|
|
231
|
+
clip: z.union([
|
|
232
|
+
z.object({ element: z.string().min(1) }),
|
|
233
|
+
z.object({ x: z.number(), y: z.number(), width: z.number().positive(), height: z.number().positive() })
|
|
234
|
+
]).optional().describe("Kareyi bir bölgeyle sınırlar: { element: '<elementId>' } o düğümün kutusunu, { x, y, width, height } elle verilen dikdörtgeni (CSS pikseli) kırpar — yan sütunu ya da tek kartı görmek için tüm sayfayı çekip elle kırpmaya gerek yok. Kutu katlanmış alanın altındaysa viewport onu kapsayana kadar BÜYÜTÜLÜR ve kutu yeniden okunur; düğüm yoksa ya da 0x0 çizildiyse çağrı boş kare yerine HATA döner."),
|
|
235
|
+
summary: z.union([z.enum(['compact', 'full']), z.literal(false)]).optional().describe("Sayfada NE GÖRÜNDÜĞÜNÜN metin özeti; varsayılan 'compact'. Her KÖK runtime düğümü için tek satır: kimlik (tip) · görünür|GİZLİ|ekran dışı · metnin ilk 80 karakteri · rows=<satır> · tab=<açık sekme> · value=<kutudaki değer> · BOŞ · errors=<sayı>; sonda toast ve hata sayfası mesajları. 'full' bunun yanına yapısal domSummary dizisini ve messages'ı koyar; false hiç toplamaz. 'Kart göründü mü, tablo doldu mu, hata var mı' sorularının cevabı buradadır — o sorular için KAREYE GEREK YOKTUR (image:'none' ile birlikte kullanın). Tip alanı runtime'ın data-component-type basmasına bağlıdır; basmıyorsa tipler yazılmaz ve summaryNote bunu söyler."),
|
|
236
|
+
diff: z.boolean().optional().describe("true: aynı sayfanın ÖNCEKİ çekimine göre ne değiştiğini döner (changes.added / removed / changed / messages) ve karenin bayt bayt aynı olup olmadığını söyler (imageUnchanged). Bellek gateway SÜRECİ içindedir: yeniden başlatılınca silinir ve ilk çağrı changes.firstCapture: true döner; sonraki çağrılar baselineAt taşır. Anahtar adres, kimlik, dil, tema, viewport, fullPage, actions, clip, scale ve biçimdir — asUser karesi gateway karesiyle karşılaştırılmaz."),
|
|
237
|
+
image: z.enum(['inline', 'none', 'ifChanged']).optional().describe("'none': kare HİÇ ÇEKİLMEZ, yanıt yalnız özet ve ölçümlerden oluşur — olgu doğrulamasının (kart göründü mü, tablo kaç satır) maliyeti bir kareden iki-üç kat küçüktür ve model bağlamına görüntü girmez. Varsayılan 'inline'. 'ifChanged': kare çekilir ama ÖNCEKİ çekimle bayt bayt aynıysa yanıta KONULMAZ (imageSkipped: true) — aynı sayfayı yeniden koştururken değişmemiş kareyi ikinci kez okumak bağlamın boşa gitmesidir; diff:true ima edilir. outPath ile 'none' birlikte verilemez (yazılacak kare üretilmiyor). Renk, hizalama, referansa sadakat gibi GÖRSEL sorular için kareyi açık bırakın."),
|
|
439
238
|
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')
|
|
440
239
|
},
|
|
441
240
|
annotations: readOnly
|
|
442
|
-
}, async ({ project, environment, applicationSlug, pageSlug, viewport, fullPage, delayMs, captureUnready, anonymous, asUser, cookies, layoutReport, actions, outPath, expectFailure, language, colorScheme }) => {
|
|
241
|
+
}, async ({ project, environment, applicationSlug, pageSlug, viewport, fullPage, delayMs, captureUnready, anonymous, asUser, cookies, layoutReport, actions, outPath, expectFailure, language, colorScheme, summary, image, format, quality, scale, clip, diff }) => {
|
|
443
242
|
try {
|
|
444
243
|
if (anonymous === true && (asUser !== undefined || cookies !== undefined)) {
|
|
445
244
|
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.");
|
|
446
245
|
}
|
|
246
|
+
if (quality !== undefined && (format ?? 'png') === 'png') {
|
|
247
|
+
throw new PlatformApiError("page_screenshot: quality yalnız format:'jpeg' ile verilir — PNG kayıpsızdır ve kalite ayarı yoktur. Küçültmek için scale (yarı çözünürlük) ya da clip (bölge) kullanın.");
|
|
248
|
+
}
|
|
249
|
+
if (image === 'none' && outPath !== undefined) {
|
|
250
|
+
throw new PlatformApiError("page_screenshot: image:'none' ile outPath birlikte verilemez — biri kareyi hiç üretmemeyi, diğeri onu dosyaya yazmayı ister. Olgu doğrulaması için yalnız image:'none', kareyi diske almak için yalnız outPath gönderin.");
|
|
251
|
+
}
|
|
252
|
+
const incompleteDrag = actions?.findIndex(step => step.action === 'drag' && (step.target === undefined || step.to === undefined)) ?? -1;
|
|
253
|
+
if (incompleteDrag >= 0) {
|
|
254
|
+
throw new PlatformApiError(`page_screenshot: actions[${incompleteDrag}] 'drag' adımı hem target (sürüklenen öğe) hem to (bırakma hedefi) ister — ör. {action:'drag', target:'kb_board__card-42', to:'kb_board__col-Assigned'}.`);
|
|
255
|
+
}
|
|
447
256
|
if (!config.studioBaseUrl) {
|
|
448
257
|
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).");
|
|
449
258
|
}
|
|
@@ -476,11 +285,12 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
|
|
|
476
285
|
const size = viewport ?? { width: 1280, height: 800 };
|
|
477
286
|
const extraCookies = Object.entries(cookies ?? {}).map(([name, value]) => ({ name, value }));
|
|
478
287
|
const identity = anonymous === true ? 'anonymous' : asUser === undefined ? 'gateway' : 'asUser';
|
|
288
|
+
const identityToken = anonymous ? undefined : resolveIdentity(asUser) ?? await platform.accessToken();
|
|
479
289
|
const cookieNames = [...new Set([...(identity === 'anonymous' ? [] : ['NG_AccessToken']), ...extraCookies.map(cookie => cookie.name)])];
|
|
480
290
|
const shot = await screenshotter.capture({
|
|
481
291
|
url,
|
|
482
292
|
apiPathMarker: `/${applicationSlug}/p/${pageSlug}`,
|
|
483
|
-
token:
|
|
293
|
+
token: identityToken,
|
|
484
294
|
...(extraCookies.length > 0 ? { cookies: extraCookies } : {}),
|
|
485
295
|
cookieUrl: config.studioBaseUrl,
|
|
486
296
|
viewport: size,
|
|
@@ -489,8 +299,78 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
|
|
|
489
299
|
...(captureUnready === true ? { captureUnready: true } : {}),
|
|
490
300
|
...(layoutReport === true ? { layoutReport: true } : {}),
|
|
491
301
|
...(colorScheme === undefined ? {} : { colorScheme }),
|
|
302
|
+
// B-2: `summary:false` + `diff` verildiğinde özet yine toplanır — fark ona dayanır ve
|
|
303
|
+
// toplanmazsa bellek hiç yazılmaz, `ifChanged` sessizce ölü bir özellik olurdu.
|
|
304
|
+
...(summary === false && !(diff === true || image === 'ifChanged') ? { summary: false } : {}),
|
|
305
|
+
...(image === 'none' ? { image: 'none' } : {}),
|
|
306
|
+
...(format === undefined ? {} : { format }),
|
|
307
|
+
...(quality === undefined ? {} : { quality }),
|
|
308
|
+
...(scale === undefined ? {} : { scale }),
|
|
309
|
+
...(clip === undefined ? {} : { clip: clip }),
|
|
492
310
|
...(actions === undefined ? {} : { actions: actions })
|
|
493
311
|
});
|
|
312
|
+
// Fark ÖZETTEN çıkar; `summary:false` verilse de fark için toplanmıştır (yanıta girmez).
|
|
313
|
+
// Anahtar KİMLİĞİ ayırır (K8): 'asUser' etiketi yetmez — iki farklı üyenin jetonu aynı
|
|
314
|
+
// etikete düşüyor ve A'nın sayfasıyla B'ninki karşılaştırılıyordu (B-3). Jeton ve çerez
|
|
315
|
+
// DEĞERLERİ anahtara girer; captureKey sha256'lar, ham değer dışarı sızmaz.
|
|
316
|
+
const wantsDiff = diff === true || image === 'ifChanged';
|
|
317
|
+
const memoryKey = captureKey({
|
|
318
|
+
url,
|
|
319
|
+
identity,
|
|
320
|
+
principal: identityToken,
|
|
321
|
+
cookieValues: extraCookies.map(cookie => `${cookie.name}=${cookie.value}`),
|
|
322
|
+
language: languageChoice?.code,
|
|
323
|
+
colorScheme,
|
|
324
|
+
viewport: size,
|
|
325
|
+
fullPage: fullPage === true,
|
|
326
|
+
actions,
|
|
327
|
+
clip,
|
|
328
|
+
scale,
|
|
329
|
+
format,
|
|
330
|
+
quality
|
|
331
|
+
});
|
|
332
|
+
const previous = wantsDiff ? captureMemory.recall(memoryKey) : undefined;
|
|
333
|
+
const currentHash = shot.png === undefined ? undefined : imageHash(shot.png);
|
|
334
|
+
const changes = !wantsDiff
|
|
335
|
+
? undefined
|
|
336
|
+
: previous === undefined
|
|
337
|
+
// `baselineAt: null` yanıt budamasında (responses.ts `prune`) düşüyordu: alan yok
|
|
338
|
+
// oluyor, geriye yalnız not kalıyordu ve "ilk kare mi, alan mı eksik" ayırt edilemiyordu.
|
|
339
|
+
? { firstCapture: true }
|
|
340
|
+
: {
|
|
341
|
+
baselineAt: previous.at,
|
|
342
|
+
...(currentHash === undefined || previous.imageHash === undefined ? {} : { imageUnchanged: currentHash === previous.imageHash }),
|
|
343
|
+
...(shot.summary === undefined ? {} : diffSummaries(previous.summary, shot.summary))
|
|
344
|
+
};
|
|
345
|
+
const imageUnchanged = changes !== undefined && 'imageUnchanged' in changes ? changes.imageUnchanged === true : false;
|
|
346
|
+
if (wantsDiff && shot.summary !== undefined) {
|
|
347
|
+
// `image:'none'` çağrısı hash üretmez; DOM hiç değişmediyse öncekini KORU, yoksa bir
|
|
348
|
+
// 'none' çağrısı aradaki taban çizgisini siliyor ve sonraki 'ifChanged' atlayamıyordu.
|
|
349
|
+
const unchangedDom = changes !== undefined && 'changed' in changes && changes.changed?.length === 0 && changes.added?.length === 0 && changes.removed?.length === 0;
|
|
350
|
+
const carried = currentHash ?? (unchangedDom ? previous?.imageHash : undefined);
|
|
351
|
+
captureMemory.remember(memoryKey, { at: new Date().toISOString(), ...(carried === undefined ? {} : { imageHash: carried }), summary: shot.summary });
|
|
352
|
+
}
|
|
353
|
+
// Notlar DİZİDE toplanır: hepsi aynı nesnede `actionsNote` anahtarını yazdığı için
|
|
354
|
+
// sonuncusu öncekileri eziyordu ve en sık durumda (sekme değişimi ağ isteği çıkarmaz)
|
|
355
|
+
// "sekme metinle eşlendi" uyarısı sessizce kayboluyordu (B-1).
|
|
356
|
+
const actionsOf = (match) => (shot.actions ?? []).filter(match);
|
|
357
|
+
const actionNotes = [];
|
|
358
|
+
const missing = actionsOf(outcome => !outcome.found);
|
|
359
|
+
const textMatchedTabs = actionsOf(outcome => outcome.action === 'tab' && outcome.matchedBy === 'text');
|
|
360
|
+
const emptyOptions = actionsOf(outcome => outcome.action === 'select' && outcome.optionsSeen === 0);
|
|
361
|
+
const unwritten = actionsOf(outcome => outcome.propWritten === false);
|
|
362
|
+
// 'wait' bir eylem değil bekleyiştir: akış sayacı ona bağlanmaz, notu tetiklemez (33/S-1).
|
|
363
|
+
const silent = actionsOf(outcome => outcome.action !== 'wait' && outcome.applied && outcome.requests === 0 && (outcome.flowRuns ?? 0) === 0);
|
|
364
|
+
if (missing.length > 0)
|
|
365
|
+
actionNotes.push(`Bulunamayan hedef var: ${missing.map(outcome => outcome.target ?? '?').join(' · ')}. elementId'leri page_schema_get ile doğrulayın; runtime düğümü data-element-id ile bulunuyor.`);
|
|
366
|
+
if (textMatchedTabs.length > 0)
|
|
367
|
+
actionNotes.push(`Sekme METİNLE eşlendi (runtime '<elementId>__tab-<sekme anahtarı>' kimliğini basmıyor): ${textMatchedTabs.map(outcome => `'${outcome.target ?? '?'}' → ${outcome.activeTab ?? '?'}`).join(' · ')}. Metin çeviriye bağlıdır; language verilen bir çekimde aynı adım kırılır.`);
|
|
368
|
+
if (emptyOptions.length > 0)
|
|
369
|
+
actionNotes.push(`Açılır kutuda hiç seçenek görülmedi: ${emptyOptions.map(outcome => `'${outcome.target ?? '?'}' (${outcome.waitedMs ?? '?'} ms beklendi)`).join(' · ')}. Adımın note alanı kutunun açılıp açılmadığını söyler: açıldıysa seçenekler bir sorgudan geliyor olabilir (query_preview), açılmadıysa kutu devre dışı ya da salt okunur olabilir.`);
|
|
370
|
+
if (unwritten.length > 0)
|
|
371
|
+
actionNotes.push(`Değer DOM'a yazıldı ama runtime prop'una ULAŞMADI: ${unwritten.map(outcome => `${outcome.action} '${outcome.target ?? '?'}'`).join(' · ')}. Kare dolu kutu gösterse de akış bu değeri okumaz — sonucu doğrulanmış saymayın; adımın note alanına bakın.`);
|
|
372
|
+
if (silent.length > 0)
|
|
373
|
+
actionNotes.push(`Uygulandı ama HİÇ AĞ İSTEĞİ çıkmadı ve HİÇBİR AKIŞ KOŞMADI: ${silent.map(outcome => `${outcome.action} '${outcome.target ?? '?'}'`).join(' · ')}. Sekme değiştirmek gibi saf istemci tarafı adımlarda bu BEKLENENDİR; veri bekleyen bir adımda olay bağı şemada görünse de akış koşmamış olabilir — bağı page_runtime_get, akış gövdesini flow_get ile doğrulayın.`);
|
|
494
374
|
const meta = {
|
|
495
375
|
url,
|
|
496
376
|
environment: resolvedEnvironment,
|
|
@@ -501,8 +381,41 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
|
|
|
501
381
|
fullPage: fullPage === true,
|
|
502
382
|
identity,
|
|
503
383
|
cookieNames,
|
|
504
|
-
|
|
384
|
+
...(shot.png === undefined
|
|
385
|
+
? { image: 'none' }
|
|
386
|
+
: {
|
|
387
|
+
sizeBytes: shot.png.byteLength,
|
|
388
|
+
format: format ?? 'png',
|
|
389
|
+
...(scale === undefined ? {} : { scale }),
|
|
390
|
+
...(shot.imageSize === undefined ? {} : { imageSize: shot.imageSize }),
|
|
391
|
+
...(shot.clip === undefined ? {} : { clip: shot.clip })
|
|
392
|
+
}),
|
|
393
|
+
...(shot.clip !== undefined && (layoutReport === true || fullPage === true)
|
|
394
|
+
? { clipNote: 'clip yalnız GÖRÜNTÜYÜ keser: layoutSummary kutuları, summaryText satırları ve contentHeight sayfanın TAMAMI için ölçülür.' }
|
|
395
|
+
: {}),
|
|
505
396
|
elapsedMs: shot.elapsedMs,
|
|
397
|
+
// S-5: yanıtın olgu katmanı. Kare açılmadan okunabilsin diye METİN olarak da döner.
|
|
398
|
+
...(shot.summary === undefined || summary === false
|
|
399
|
+
? {}
|
|
400
|
+
: {
|
|
401
|
+
summaryText: summaryLines(shot.summary).join('\n'),
|
|
402
|
+
summaryStats: summaryStats(shot.summary),
|
|
403
|
+
...(summary === 'full' ? { domSummary: shot.summary.nodes, messages: shot.summary.messages } : {}),
|
|
404
|
+
...(shot.summary.truncated ? { summaryTruncated: true, summaryTruncatedNote: 'Sayfada 300 kök düğümden fazlası var; özet ilk 300 düğümde kesildi. Daha dar bakmak için sayfayı bölerek doğrulayın.' } : {}),
|
|
405
|
+
...(shot.summary.typed ? {} : { summaryNote: "Runtime data-component-type basmıyor (eski runtime ya da frontend ayağı dağıtılmadı) — satırlarda komponent TİPİ yazılmadı; kimlik ve görünürlük yine doğrudur." })
|
|
406
|
+
}),
|
|
407
|
+
...(shot.summaryUnavailable === undefined ? {} : { summaryUnavailable: shot.summaryUnavailable }),
|
|
408
|
+
...(changes === undefined
|
|
409
|
+
? {}
|
|
410
|
+
: {
|
|
411
|
+
changes,
|
|
412
|
+
...('firstCapture' in changes
|
|
413
|
+
? { changesNote: 'Bu adresin ilk karesi — karşılaştırılacak önceki çekim yok. Bellek gateway süreci içindedir ve yeniden başlatılınca silinir.' }
|
|
414
|
+
: {}),
|
|
415
|
+
...('imageUnchanged' in changes && changes.imageUnchanged === false && 'changed' in changes && changes.changed?.length === 0 && changes.added?.length === 0 && changes.removed?.length === 0
|
|
416
|
+
? { changesNote: 'Kare bayt bayt değişti ama DOM özeti aynı: fark piksel düzeyinde (yazı tipi yerleşmesi, animasyon, imleç). Yapısal bir değişiklik yok.' }
|
|
417
|
+
: {})
|
|
418
|
+
}),
|
|
506
419
|
...(shot.contentHeight === undefined ? {} : { contentHeight: shot.contentHeight }),
|
|
507
420
|
...(shot.fullPageTruncated
|
|
508
421
|
? { 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.` }
|
|
@@ -555,13 +468,7 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
|
|
|
555
468
|
runtimeErrors: shot.runtimeErrors,
|
|
556
469
|
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.`
|
|
557
470
|
}),
|
|
558
|
-
...(
|
|
559
|
-
? { 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.` }
|
|
560
|
-
: {}),
|
|
561
|
-
// 'wait' bir eylem değil bekleyiştir: akış sayacı ona bağlanmaz, notu tetiklemez (33/S-1).
|
|
562
|
-
...(shot.actions?.some(outcome => outcome.action !== 'wait' && outcome.applied && outcome.requests === 0 && (outcome.flowRuns ?? 0) === 0)
|
|
563
|
-
? { actionsNote: `Uygulandı ama HİÇ AĞ İSTEĞİ çıkmadı ve HİÇBİR AKIŞ KOŞMADI: ${shot.actions.filter(outcome => outcome.action !== 'wait' && 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.` }
|
|
564
|
-
: {}),
|
|
471
|
+
...(actionNotes.length === 0 ? {} : { actionsNote: actionNotes.join('\n') }),
|
|
565
472
|
...(shot.layout === undefined
|
|
566
473
|
? {}
|
|
567
474
|
: shot.layout.length === 0
|
|
@@ -576,6 +483,8 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
|
|
|
576
483
|
try {
|
|
577
484
|
// B-6: klasör yoksa ENOENT ile düşüyordu; koşu ajanı her seferinde klasörü elle açıyordu.
|
|
578
485
|
await mkdir(dirname(absolute), { recursive: true });
|
|
486
|
+
if (shot.png === undefined)
|
|
487
|
+
throw new PlatformApiError('page_screenshot: yazılacak kare üretilmedi (image kipi kareyi atladı) — outPath ile birlikte kare üreten bir kip kullanın.');
|
|
579
488
|
await writeFile(absolute, shot.png);
|
|
580
489
|
}
|
|
581
490
|
catch (error) {
|
|
@@ -584,12 +493,19 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
|
|
|
584
493
|
}
|
|
585
494
|
return toolResult({ path: absolute, ...meta }, config);
|
|
586
495
|
}
|
|
496
|
+
// image:'none' — kare hiç üretilmedi; yanıt yalnız olgu katmanıdır.
|
|
497
|
+
if (shot.png === undefined)
|
|
498
|
+
return toolResult(meta, config);
|
|
499
|
+
// 'ifChanged' — kare çekildi ama öncekiyle AYNI: bağlama ikinci kez girmesin.
|
|
500
|
+
if (image === 'ifChanged' && imageUnchanged) {
|
|
501
|
+
return toolResult({ ...meta, imageSkipped: true, imageSkippedNote: 'Kare öncekiyle bayt bayt aynı olduğu için yanıta konulmadı (image: ifChanged). Yine de görmek için image varsayılanına dönün.' }, config);
|
|
502
|
+
}
|
|
587
503
|
if (shot.png.byteLength > maxInlinePngBytes) {
|
|
588
|
-
throw new PlatformApiError(`Görüntü ${shot.png.byteLength} B — inline sınırı (${maxInlinePngBytes} B) aşıyor. outPath verin (dosyaya yazılır, model bağlamına girmez) ya da fullPage'i kapatıp viewport'u küçültün.`);
|
|
504
|
+
throw new PlatformApiError(`Görüntü ${shot.png.byteLength} B — inline sınırı (${maxInlinePngBytes} B) aşıyor. outPath verin (dosyaya yazılır, model bağlamına girmez), image:'none' ile yalnız özet alın ya da fullPage'i kapatıp viewport'u küçültün.`);
|
|
589
505
|
}
|
|
590
506
|
return {
|
|
591
507
|
content: [
|
|
592
|
-
{ type: 'image', data: shot.png.toString('base64'), mimeType: 'image/png' },
|
|
508
|
+
{ type: 'image', data: shot.png.toString('base64'), mimeType: shot.mimeType ?? 'image/png' },
|
|
593
509
|
{ type: 'text', text: JSON.stringify(meta) }
|
|
594
510
|
],
|
|
595
511
|
structuredContent: meta
|
|
@@ -601,7 +517,7 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
|
|
|
601
517
|
});
|
|
602
518
|
server.registerTool('page_schema_get', {
|
|
603
519
|
title: 'Sayfa şemasını oku',
|
|
604
|
-
description: "Sayfanın komponent şemasını okur
|
|
520
|
+
description: "Sayfanın komponent şemasını okur. Yanıttaki version değerini page_schema_patch'e baseVersion olarak verin: sayfa o okumadan beri değiştiyse yama 409 ile reddedilir ve hiçbir op uygulanmaz.",
|
|
605
521
|
inputSchema: {
|
|
606
522
|
project: projectParam,
|
|
607
523
|
environment: environmentParam,
|
|
@@ -614,17 +530,18 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
|
|
|
614
530
|
}, handle(config, async ({ project, environment, applicationId, pageId, mode, componentId }) => {
|
|
615
531
|
const page = await fetchPage(platform, project, environment, applicationId, pageId);
|
|
616
532
|
const updatedAt = page.audit?.updatedAt ?? page.audit?.createdAt;
|
|
533
|
+
const version = page.version;
|
|
617
534
|
if (mode === 'full')
|
|
618
|
-
return { updatedAt, schema: page.schema ?? [] };
|
|
535
|
+
return { version, updatedAt, schema: page.schema ?? [] };
|
|
619
536
|
if (mode === 'subtree') {
|
|
620
537
|
if (!componentId)
|
|
621
538
|
throw new PlatformApiError("mode='subtree' için componentId zorunludur.");
|
|
622
539
|
const subtree = findNode(page.schema, componentId);
|
|
623
540
|
if (!subtree)
|
|
624
541
|
throw new PlatformApiError(`Sayfada '${componentId}' id'li bir komponent yok; page_schema_get'i summary kipinde çağırıp hedefi doğrulayın.`);
|
|
625
|
-
return { updatedAt, schema: subtree };
|
|
542
|
+
return { version, updatedAt, schema: subtree };
|
|
626
543
|
}
|
|
627
|
-
return { updatedAt, schema: (page.schema ?? []).map(node => summarize(node)) };
|
|
544
|
+
return { version, updatedAt, schema: (page.schema ?? []).map(node => summarize(node)) };
|
|
628
545
|
}));
|
|
629
546
|
server.registerTool('page_state', {
|
|
630
547
|
title: 'Sayfa durumunu getir',
|
|
@@ -642,26 +559,33 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
|
|
|
642
559
|
const propTypeIssues = setPropTypeErrors(allOps.filter(entry => entry.op === 'set_prop_type'));
|
|
643
560
|
if (propTypeIssues.length > 0)
|
|
644
561
|
throw new PlatformApiError(`set_prop_type gövdesi eksik (istek gönderilmedi): ${propTypeIssues.join(' · ')}`, 400);
|
|
645
|
-
//
|
|
646
|
-
//
|
|
647
|
-
const opRecords = allOps.filter(entry =>
|
|
562
|
+
// Kuru koşu ile yazma AYNI gövdeyi görür (P0-2 Step 3): resize_component burada da sunucu
|
|
563
|
+
// op'larına çevrilir, platform dokunulan düğümleri aynı kapsamla doğrular.
|
|
564
|
+
const opRecords = allOps.filter(entry => entry.op !== 'resize_component');
|
|
648
565
|
const normalized = normalizeSchemaOps(opRecords);
|
|
649
|
-
const validationIssues =
|
|
566
|
+
const validationIssues = [
|
|
567
|
+
...schemaOpValidationErrors(opRecords).map(issue => `PAGE-016 — ${issue}`),
|
|
568
|
+
...codePropTemplateErrors(opRecords).map(issue => `PAGE-032 — ${issue}`)
|
|
569
|
+
];
|
|
570
|
+
const ordering = await orderByIndex(opRecords, () => fetchPage(platform, project, environment, applicationId, pageId));
|
|
650
571
|
const response = ((await platform.tenant('POST', project, `/pages/${applicationId}/${pageId}/schema/validate`, {
|
|
651
572
|
environment,
|
|
652
|
-
body: { ops:
|
|
573
|
+
body: { ops: [...allOps.filter(entry => entry.op === 'resize_component').flatMap(resizeServerOps), ...ordering.ops] }
|
|
653
574
|
})) ?? {});
|
|
654
575
|
if (validationIssues.length > 0)
|
|
655
|
-
response.validationWarnings = validationIssues
|
|
576
|
+
response.validationWarnings = validationIssues;
|
|
577
|
+
if (ordering.warnings.length > 0)
|
|
578
|
+
response.layoutWarnings = ordering.warnings;
|
|
656
579
|
return withNormalization(response, [
|
|
657
580
|
...componentDefaultsNote(normalized.components),
|
|
658
581
|
...validationNote(normalized.validations),
|
|
659
|
-
...replacePropNote(normalized.props)
|
|
582
|
+
...replacePropNote(normalized.props),
|
|
583
|
+
...flexIndexOrderNote(ordering.moved)
|
|
660
584
|
]);
|
|
661
585
|
}));
|
|
662
586
|
server.registerTool('page_schema_patch', {
|
|
663
587
|
title: 'Sayfa şemasına patch uygula',
|
|
664
|
-
description: "Micro-op'ları doğrulama duvarından geçirip kaydeder; açık studio canvas'ları SignalR ile canlı güncellenir.
|
|
588
|
+
description: "Micro-op'ları doğrulama duvarından geçirip TEK PATCH ile kaydeder; açık studio canvas'ları SignalR ile canlı güncellenir. İstek atomiktir: bir op reddedilirse hiçbiri yazılmaz. baseVersion (page_schema_get çıktısındaki version ya da önceki yamanın newVersion'ı) verilirse sayfa o sürümden beri değişmişse 409 döner ve hiçbir op uygulanmaz (stale plan koruması); verilmezse gateway yalnız bu çağrıda zaten okuduğu sayfanın sürümünü gönderir. 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. Gateway ön kontrolleri (PAGE-006/008/016/032, set_prop_type gövdesi, move_component size) op başına toplanır: düşen HER op tek hata yanıtında rejectedOps[] ({ index, op, target, code, message }) ile listelenir ve hiçbir op gönderilmez — hepsini düzeltip listeyi bir kez yeniden gönderin.",
|
|
665
589
|
inputSchema: {
|
|
666
590
|
project: projectParam,
|
|
667
591
|
environment: environmentParam,
|
|
@@ -669,28 +593,31 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
|
|
|
669
593
|
pageId: guid,
|
|
670
594
|
ops: opsParam.optional(),
|
|
671
595
|
operations: opsParam.optional().describe("'ops' ile aynı alan (eşanlamlı) — entity/microservice/workflow patch'leriyle aynı adı kullanabilmeniz için"),
|
|
672
|
-
|
|
596
|
+
baseVersion: z.string().optional().describe("page_schema_get çıktısındaki version ya da önceki page_schema_patch yanıtındaki newVersion; sayfa o sürümden beri değiştiyse 409 döner ve hiçbir op uygulanmaz"),
|
|
597
|
+
expectedUpdatedAt: z.string().optional().describe("Eski koruma: page_schema_get çıktısındaki updatedAt; eşleşmezse patch uygulanmaz. baseVersion tercih edilir"),
|
|
673
598
|
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."),
|
|
674
599
|
view: writeViewParam
|
|
675
600
|
},
|
|
676
601
|
annotations: updates
|
|
677
|
-
}, handle(config, async ({ project, environment, applicationId, pageId, ops, operations, expectedUpdatedAt, allowFlowDeletion, view }) => {
|
|
602
|
+
}, handle(config, async ({ project, environment, applicationId, pageId, ops, operations, baseVersion, expectedUpdatedAt, allowFlowDeletion, view }) => {
|
|
678
603
|
const allOps = microOps('page_schema_patch', ops, operations).map(op => op);
|
|
679
604
|
const removeEventOps = allOps.filter(entry => entry.op === 'remove_event');
|
|
680
605
|
const resizeOps = allOps.filter(entry => entry.op === 'resize_component');
|
|
681
606
|
const propTypeOps = allOps.filter(entry => entry.op === 'set_prop_type');
|
|
682
607
|
const opRecords = allOps.filter(entry => !['remove_event', 'resize_component', 'set_prop_type'].includes(String(entry.op)));
|
|
683
|
-
const
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
608
|
+
const rejected = [];
|
|
609
|
+
const reject = (code, entry, messages) => {
|
|
610
|
+
for (const message of messages) {
|
|
611
|
+
rejected.push({ index: allOps.indexOf(entry), op: String(entry.op ?? ''), target: typeof entry.target === 'string' ? entry.target : null, code, message });
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
for (const entry of propTypeOps)
|
|
615
|
+
reject('SET-PROP-TYPE', entry, setPropTypeErrors([entry]));
|
|
616
|
+
for (const entry of opRecords.filter(record => record.op === 'move_component' && record.size !== undefined)) {
|
|
617
|
+
reject('MOVE-SIZE', entry, ["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'."]);
|
|
691
618
|
}
|
|
692
|
-
|
|
693
|
-
|
|
619
|
+
for (const entry of opRecords.filter(record => (record.op === 'remove_component' || record.op === 'move_component') && record.target === PAGE_CONTAINER_ELEMENT_ID)) {
|
|
620
|
+
reject('PAGE-006', entry, [`'${PAGE_CONTAINER_ELEMENT_ID}' sayfanın kök kapsayıcısıdır; silinmez ve taşınmaz — 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).`]);
|
|
694
621
|
}
|
|
695
622
|
let cachedPage;
|
|
696
623
|
const readPage = async () => {
|
|
@@ -705,19 +632,10 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
|
|
|
705
632
|
}
|
|
706
633
|
}
|
|
707
634
|
const normalized = normalizeSchemaOps(opRecords);
|
|
708
|
-
const
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
}
|
|
713
|
-
const wantsRootAdd = opRecords.some(entry => entry.op === 'add_component' && (entry.parent === undefined || entry.parent === null || entry.parent === 'root'));
|
|
714
|
-
let retargeted = [];
|
|
715
|
-
if (wantsRootAdd) {
|
|
716
|
-
const page = await readPage();
|
|
717
|
-
if ((page.schema ?? []).some(node => node.elementId === PAGE_CONTAINER_ELEMENT_ID)) {
|
|
718
|
-
retargeted = retargetRootAdds(opRecords, PAGE_CONTAINER_ELEMENT_ID);
|
|
719
|
-
}
|
|
720
|
-
}
|
|
635
|
+
for (const entry of opRecords)
|
|
636
|
+
reject('PAGE-016', entry, schemaOpValidationErrors([entry]));
|
|
637
|
+
for (const entry of opRecords)
|
|
638
|
+
reject('PAGE-032', entry, codePropTemplateErrors([entry]));
|
|
721
639
|
if (opRecords.some(entry => entry.op === 'set_event')) {
|
|
722
640
|
const typesByElementId = new Map();
|
|
723
641
|
try {
|
|
@@ -728,14 +646,27 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
|
|
|
728
646
|
}
|
|
729
647
|
collectTypes(opRecords.map(entry => entry.component).filter(Boolean), typesByElementId);
|
|
730
648
|
const eventsBySlug = await fetchComponentEventNames(platform, project, environment, applicationId).catch(() => new Map());
|
|
731
|
-
const
|
|
732
|
-
|
|
733
|
-
|
|
649
|
+
for (const entry of opRecords)
|
|
650
|
+
reject('PAGE-008', entry, unknownEventOps([entry], typesByElementId, eventsBySlug));
|
|
651
|
+
}
|
|
652
|
+
if (rejected.length > 0)
|
|
653
|
+
throw new PlatformApiError(rejectedOpsMessage(rejected), 400);
|
|
654
|
+
const wantsRootAdd = opRecords.some(entry => entry.op === 'add_component' && (entry.parent === undefined || entry.parent === null || entry.parent === 'root'));
|
|
655
|
+
let retargeted = [];
|
|
656
|
+
if (wantsRootAdd) {
|
|
657
|
+
const page = await readPage();
|
|
658
|
+
if ((page.schema ?? []).some(node => node.elementId === PAGE_CONTAINER_ELEMENT_ID)) {
|
|
659
|
+
retargeted = retargetRootAdds(opRecords, PAGE_CONTAINER_ELEMENT_ID);
|
|
660
|
+
}
|
|
734
661
|
}
|
|
662
|
+
const ordering = await orderByIndex(opRecords, readPage);
|
|
663
|
+
const sentOps = ordering.ops;
|
|
735
664
|
// Akış silen op'lar yamadan ÖNCE hesaplanır: uyarı sonradan geldiğinde akış zaten kalıcı
|
|
736
665
|
// silinmiş oluyordu ve "önce yedekle" tavsiyesi uygulanamıyordu (09-05 koşusu, G-21).
|
|
737
666
|
let deletedFlows = [];
|
|
738
|
-
|
|
667
|
+
// remove_event opRecords'ta değil, ayrı listede durur; koşul yalnız opRecords'a baktığı sürece
|
|
668
|
+
// yalnız olay kaldıran bir yama bu korumayı hiç tetiklemiyordu (2026-09-14).
|
|
669
|
+
if ([...opRecords, ...removeEventOps].some(entry => ['remove_component', 'set_event', 'remove_event'].includes(String(entry.op)))) {
|
|
739
670
|
try {
|
|
740
671
|
const page = await readPage();
|
|
741
672
|
deletedFlows = orphanedFlowPlan(page.schema, page.events, [...opRecords, ...removeEventOps]);
|
|
@@ -755,6 +686,7 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
|
|
|
755
686
|
const formWarnings = [...formBindingWarnings(opRecords), ...formSubmitButtonWarnings(opRecords)];
|
|
756
687
|
let flowWarnings = [];
|
|
757
688
|
let layoutWarnings = [];
|
|
689
|
+
let flowsBeforePatch;
|
|
758
690
|
if (opRecords.some(entry => entry.op === 'remove_component')) {
|
|
759
691
|
try {
|
|
760
692
|
const page = await readPage();
|
|
@@ -768,7 +700,8 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
|
|
|
768
700
|
try {
|
|
769
701
|
const hosted = await platform.tenant('GET', project, flowPath('page', applicationId, pageId), { environment });
|
|
770
702
|
const page = await readPage();
|
|
771
|
-
|
|
703
|
+
flowsBeforePatch = hosted?.data ?? [];
|
|
704
|
+
flowWarnings = [...flowWarnings, ...staleSetterWarnings(flowsBeforePatch, opRecords, page.schema)];
|
|
772
705
|
}
|
|
773
706
|
catch {
|
|
774
707
|
// akış listesi okunamadı
|
|
@@ -781,7 +714,7 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
|
|
|
781
714
|
try {
|
|
782
715
|
const page = await readPage();
|
|
783
716
|
const simulated = JSON.parse(JSON.stringify(page.schema ?? []));
|
|
784
|
-
const touched = simulateLayoutOps(simulated,
|
|
717
|
+
const touched = simulateLayoutOps(simulated, sentOps);
|
|
785
718
|
// PAGE-013 op'lar UYGULANDIKTAN SONRAKİ ağaca bakar: tek başına eklenen düğüm masum
|
|
786
719
|
// görünür, iç içeliği ancak hedef parent zinciri belli olunca anlarsın.
|
|
787
720
|
nestedAfterOps = nestedRepeaterNodes(simulated);
|
|
@@ -790,7 +723,8 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
|
|
|
790
723
|
...verticalGapWarnings(simulated, 'PAGE-010 (uyarı)', touched),
|
|
791
724
|
...stringStyleWarning(stringStyleNodes(simulated)),
|
|
792
725
|
...inertFlexPropWarning(inertFlexPropNodes(simulated)),
|
|
793
|
-
...flexSizeWarnings(simulated, 'PAGE-017 (uyarı)', 'PAGE-018 (uyarı)')
|
|
726
|
+
...flexSizeWarnings(simulated, 'PAGE-017 (uyarı)', 'PAGE-018 (uyarı)'),
|
|
727
|
+
...spacingLayerWarnings(simulated, 'PAGE-029 (uyarı)', 'PAGE-030 (uyarı)')
|
|
794
728
|
];
|
|
795
729
|
}
|
|
796
730
|
catch {
|
|
@@ -799,30 +733,25 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
|
|
|
799
733
|
}
|
|
800
734
|
if (nestedAfterOps.length > 0)
|
|
801
735
|
throw new PlatformApiError(nestedRepeaterMessage(nestedAfterOps), 400);
|
|
802
|
-
const removedEvents = removeEventOps
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
if (propTypeOps.length > 0)
|
|
816
|
-
cachedPage = undefined;
|
|
817
|
-
const response = opRecords.length > 0
|
|
818
|
-
? ((await platform.tenant('PATCH', project, `/pages/${applicationId}/${pageId}/schema`, { environment, body: { ops: opRecords } })) ?? {})
|
|
819
|
-
: { valid: true };
|
|
736
|
+
const removedEvents = planEventRemovals(removeEventOps);
|
|
737
|
+
const resized = resizeOps.length > 0 ? planResizes((await readPage()).schema ?? [], resizeOps) : { serverOps: [], applied: [], warnings: [] };
|
|
738
|
+
const propTypes = propTypeOps.length > 0 ? planPropTypes((await readPage()).schema ?? [], propTypeOps) : { serverOps: [], applied: [] };
|
|
739
|
+
const serverOps = [...removeEventOps, ...resized.serverOps, ...propTypes.serverOps, ...sentOps];
|
|
740
|
+
layoutWarnings = [...ordering.warnings, ...layoutWarnings];
|
|
741
|
+
// Tek istek, tek sürüm: öykünülen op'lar 2026-09-14'e kadar ayrı GET + tam PUT çiftleriydi ve
|
|
742
|
+
// yamadan ÖNCE yazılıyordu — PATCH reddedilse bile kalıcıydılar. Artık hepsi aynı atomik PATCH'te.
|
|
743
|
+
// baseVersion yalnız biliniyorsa gider (ajanın verdiği ya da bu çağrıda zaten okunan sayfanınki):
|
|
744
|
+
// sürümü öğrenmek için ayrı GET atılmaz, platform satırı her yamada kilitler.
|
|
745
|
+
const response = ((await platform.tenant('PATCH', project, `/pages/${applicationId}/${pageId}/schema`, {
|
|
746
|
+
environment,
|
|
747
|
+
body: { ops: serverOps, baseVersion: baseVersion ?? (typeof cachedPage?.version === 'string' ? cachedPage.version : undefined) }
|
|
748
|
+
})) ?? {});
|
|
820
749
|
if (view !== 'full')
|
|
821
750
|
outlineWriteResponse(response);
|
|
822
751
|
if (removedEvents.length > 0)
|
|
823
752
|
response.removedEvents = removedEvents;
|
|
824
|
-
if (propTypes.length > 0)
|
|
825
|
-
response.propTypes = propTypes;
|
|
753
|
+
if (propTypes.applied.length > 0)
|
|
754
|
+
response.propTypes = propTypes.applied;
|
|
826
755
|
if (resized.applied.length > 0)
|
|
827
756
|
response.resized = resized.applied;
|
|
828
757
|
if (resized.warnings.length > 0)
|
|
@@ -833,12 +762,25 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
|
|
|
833
762
|
response.formWarnings = formWarnings;
|
|
834
763
|
if (flowWarnings.length > 0)
|
|
835
764
|
response.flowWarnings = flowWarnings;
|
|
765
|
+
// FLOW-018 yazma sonrası (G-04): uyarı yamadan önce "kopabilir" der; kopup kopmadığını ancak
|
|
766
|
+
// akışları yeniden okumak söyler. Okuma başarısız olursa yama yine geçerlidir.
|
|
767
|
+
if (flowsBeforePatch && flowWarnings.length > 0 && opRecords.some(entry => entry.op === 'add_component')) {
|
|
768
|
+
try {
|
|
769
|
+
const hosted = await platform.tenant('GET', project, flowPath('page', applicationId, pageId), { environment });
|
|
770
|
+
const lost = lostSetterTargets(flowsBeforePatch, hosted?.data ?? [], opRecords);
|
|
771
|
+
if (lost.length > 0)
|
|
772
|
+
response.flowTargetsLost = lost;
|
|
773
|
+
}
|
|
774
|
+
catch {
|
|
775
|
+
// akış listesi yeniden okunamadı
|
|
776
|
+
}
|
|
777
|
+
}
|
|
836
778
|
if (layoutWarnings.length > 0)
|
|
837
779
|
response.layoutWarnings = layoutWarnings;
|
|
838
780
|
// G-23 micro-op ayağı: yama 200 döner ve `appliedPatch` op'un ŞEMAYA yazıldığını söyler, ama
|
|
839
781
|
// alanın gerçekten KAYDEDİLDİĞİNİ söylemez. Yamadan sonra şemayı bir kez okuyup op'un yazmak
|
|
840
782
|
// istediğiyle kayıtlı olanı karşılaştırıyoruz. Okuma başarısız olursa yama yine geçerlidir.
|
|
841
|
-
const contentOps =
|
|
783
|
+
const contentOps = sentOps.filter(entry => entry.op === 'add_component' || entry.op === 'replace_prop');
|
|
842
784
|
let opDrops = [];
|
|
843
785
|
if (contentOps.length > 0) {
|
|
844
786
|
try {
|
|
@@ -854,7 +796,8 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
|
|
|
854
796
|
...componentDefaultsNote(normalized.components),
|
|
855
797
|
...validationNote(normalized.validations),
|
|
856
798
|
...catalogNotes,
|
|
857
|
-
...replacePropNote(normalized.props)
|
|
799
|
+
...replacePropNote(normalized.props),
|
|
800
|
+
...flexIndexOrderNote(ordering.moved)
|
|
858
801
|
]), opDrops);
|
|
859
802
|
}));
|
|
860
803
|
server.registerTool('page_create', {
|
|
@@ -914,7 +857,7 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
|
|
|
914
857
|
saved.schema = saved.schema.map(node => summarize(node));
|
|
915
858
|
}
|
|
916
859
|
const normalized = withWriteDiff(withNormalization(response, [...container.fixes, ...reset, ...componentDefaultsNote(fixed), ...catalogNotes, ...reorderNote(moved)]), drops);
|
|
917
|
-
const createWarnings = [...gaps, ...stringStyleWarning(stringStyles), ...inertFlexPropWarning(inertFlexPropNodes(container.schema)), ...flexSizeWarnings(container.schema, 'PAGE-017 (uyarı)', 'PAGE-018 (uyarı)')];
|
|
860
|
+
const createWarnings = [...gaps, ...stringStyleWarning(stringStyles), ...inertFlexPropWarning(inertFlexPropNodes(container.schema)), ...flexSizeWarnings(container.schema, 'PAGE-017 (uyarı)', 'PAGE-018 (uyarı)'), ...spacingLayerWarnings(container.schema, 'PAGE-029 (uyarı)', 'PAGE-030 (uyarı)')];
|
|
918
861
|
if (createWarnings.length === 0 || !normalized || typeof normalized !== 'object')
|
|
919
862
|
return normalized;
|
|
920
863
|
return { ...normalized, layoutWarnings: createWarnings };
|
|
@@ -948,7 +891,7 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
|
|
|
948
891
|
const moved = overrides.schema === undefined ? [] : reorderSchemaBlocks(overrides.schema);
|
|
949
892
|
const gaps = overrides.schema === undefined
|
|
950
893
|
? []
|
|
951
|
-
: [...verticalGapWarnings(overrides.schema, 'PAGE-010 (uyarı)'), ...inertFlexPropWarning(inertFlexPropNodes(overrides.schema)), ...flexSizeWarnings(overrides.schema, 'PAGE-017 (uyarı)', 'PAGE-018 (uyarı)')];
|
|
894
|
+
: [...verticalGapWarnings(overrides.schema, 'PAGE-010 (uyarı)'), ...inertFlexPropWarning(inertFlexPropNodes(overrides.schema)), ...flexSizeWarnings(overrides.schema, 'PAGE-017 (uyarı)', 'PAGE-018 (uyarı)'), ...spacingLayerWarnings(overrides.schema, 'PAGE-029 (uyarı)', 'PAGE-030 (uyarı)')];
|
|
952
895
|
const body = pageUpdateBody(current, overrides);
|
|
953
896
|
await platform.tenant('PUT', project, `/pages/${applicationId}/${pageId}`, { environment, body });
|
|
954
897
|
const summary = {
|