@appaflytech/zeyrek-ai-mcp 0.0.8 → 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/componentCatalog.js +122 -27
- package/dist/knowledge/componentEvents.js +35 -4
- package/dist/knowledge/errorPath.js +1 -1
- package/dist/knowledge/normalize.js +15 -0
- package/dist/knowledge/rules.js +3 -1
- package/dist/knowledge/styleResolve.js +46 -4
- 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/screenshot.js +414 -23
- package/dist/platform/screenshotMemory.js +55 -0
- package/dist/resources/examples/layout-app-shell.json +1 -1
- package/dist/resources/examples/theme-brand.json +162 -0
- package/dist/resources/guides/component.md +7 -1
- package/dist/resources/guides/getting-started.md +4 -0
- package/dist/resources/guides/layout.md +30 -26
- package/dist/resources/guides/microservice.md +55 -1
- package/dist/resources/guides/page.md +87 -44
- package/dist/resources/guides/theme.md +210 -0
- package/dist/resources/guides/translation.md +6 -4
- package/dist/resources/guides/workflow.md +91 -6
- package/dist/resources/schema-fingerprint.json +2 -2
- package/dist/server.js +52 -43
- package/dist/tools/components.js +8 -0
- package/dist/tools/designSchemaPatch.js +152 -0
- package/dist/tools/flows.js +15 -18
- package/dist/tools/layouts.js +4 -4
- package/dist/tools/microservices.js +5 -5
- package/dist/tools/pages.js +196 -323
- package/dist/tools/projects.js +66 -9
- 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 +1 -1
- 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, lostSetterTargets, normalizeComponentDefaults, normalizeSchemaOps, schemaOpValidationErrors, codePropTemplateErrors, validationNote, componentDefaultsNote, replacePropNote, ensurePageContainer, retargetRootAdds, rootAddsNote, flexSizeWarnings, spacingLayerWarnings, 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,
|
|
@@ -35,130 +38,6 @@ function runtimeFunctions(value) {
|
|
|
35
38
|
.map(parameter => ({ name: parameter.name, required: parameter.required === true, defaultValue: parameter.defaultValue ?? null }))
|
|
36
39
|
}));
|
|
37
40
|
}
|
|
38
|
-
/**
|
|
39
|
-
* Yazma yanıtındaki şema gövdesinin biçimi (B-55 · S-9). Varsayılan `outline`: gövde ajanın
|
|
40
|
-
* aynı turda kendi gönderdiği içeriğin yankısıdır, kanıt ise pointer'da ve `silentlyDropped`ta
|
|
41
|
-
* durur. Bayrağı bilmeyen ajan pahalı olanı ödemesin diye kırpma OPT-OUT'tur (aynı karar:
|
|
42
|
-
* `DS-004`, `datasource_configuration_options`).
|
|
43
|
-
*/
|
|
44
|
-
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.");
|
|
45
|
-
/**
|
|
46
|
-
* `appliedPatch` zarfın altında da gelebilir (`{ data: { appliedPatch } }`), düz de.
|
|
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} (value ham değer, code sarmalı {type: 'code', ref: '{{ … }}'} ya da translation sarmalı {type: 'translation', ref: '<çeviri anahtarı>'} — prop tipini değiştirmek için komponenti silip yeniden EKLEMEYİN, ona yazan setter'ların hedefi silinir (FLOW-018); platform replace_prop'ta expression alanını OKUMAZ — gateway onu code sarmalına çevirir) | 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}} (index ÇİZİM sırası değildir, runtime çocukları blocks.position'a göre (y, sonra x) çizer: flex kapta gateway index'i görünen sıraya çevirir ve kardeş konumlarını yeniden yazar; ızgara kapta sırayı position.y verir, sona eklemeyen index PAGE-033 uyarısıdır) | 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) | set_appearance {target, appearance: {heightType?, widthType?, tooltip?, background?, …}} (kurulu düğümün appearance'ına YALNIZ verilen alanları yazar, gerisini korur; null alanı temizler; bilinmeyen alan reddedilir. GÖRÜNÜRLÜK appearance'ta DEĞİLDİR: gizlemek için replace_prop {target, prop: 'isHidden', value: true}, koşul için set_binding {prop: 'isHidden'} — add_component gövdesindeki appearance.isHidden'ı gateway props.isHidden'a taşır) | 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 sarmalıyla aynı sonucu verir ve mevcut data'yı tasarım yedeği olarak taşır. 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
|
-
/**
|
|
76
|
-
* Ön kontroller eskiden İLK hatada fırlatıyordu: dört op'u yanlış olan bir yama dört ayrı turda
|
|
77
|
-
* ancak görülüyordu ve her tur tüm planı yeniden gönderiyordu (S-11, 2026-09-15). Artık op başına
|
|
78
|
-
* toplanır ve tek yanıtta döner. Yama yine ATOMİKTİR — birbirine bağlı op'lar (add_component +
|
|
79
|
-
* set_event) yarım kalmasın diye geçenler de gönderilmez.
|
|
80
|
-
*/
|
|
81
|
-
function rejectedOpsMessage(unordered) {
|
|
82
|
-
const rejected = [...unordered].sort((left, right) => left.index - right.index);
|
|
83
|
-
const lines = rejected.map(entry => `#${entry.index} ${entry.op}${entry.target ? ` '${entry.target}'` : ''} [${entry.code}]: ${entry.message}`);
|
|
84
|
-
const validationHint = rejected.some(entry => entry.code === 'PAGE-016')
|
|
85
|
-
? " PAGE-016 çalışan biçimi dört alanı da taşır: { type: 'required'|'email'|'regex'|'max-length'|'min-length'|'max-value'|'min-value'|'value-options', message: '<kullanıcıya görünen metin>', value: <eşik/desen/true>, isActive: true }."
|
|
86
|
-
: '';
|
|
87
|
-
return `Ön kontrol ${rejected.length} op'u reddetti; yama atomik olduğu için HİÇBİR op gönderilmedi (istek gönderilmedi). Düşen op'ları düzeltip TÜM listeyi tek çağrıda yeniden gönderin.${validationHint}\n${lines.join('\n')}\nrejectedOps: ${JSON.stringify(rejected)}`;
|
|
88
|
-
}
|
|
89
|
-
function collectTypes(nodes, into) {
|
|
90
|
-
for (const node of nodes ?? []) {
|
|
91
|
-
if (typeof node.elementId === 'string' && typeof node.type === 'string')
|
|
92
|
-
into.set(node.elementId, node.type);
|
|
93
|
-
collectTypes(node.children, into);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
function removeNodeFromTree(nodes, target, parent) {
|
|
97
|
-
for (let index = 0; index < nodes.length; index++) {
|
|
98
|
-
if (nodes[index].elementId === target)
|
|
99
|
-
return { node: nodes.splice(index, 1)[0], parent };
|
|
100
|
-
const found = removeNodeFromTree(nodes[index].children ?? [], target, nodes[index]);
|
|
101
|
-
if (found)
|
|
102
|
-
return found;
|
|
103
|
-
}
|
|
104
|
-
return undefined;
|
|
105
|
-
}
|
|
106
|
-
function setDesktopPosition(node, position) {
|
|
107
|
-
const blocks = Array.isArray(node.blocks) ? node.blocks : [];
|
|
108
|
-
const block = blocks.find(entry => Number(entry?.breakpoint) === 1024);
|
|
109
|
-
if (block)
|
|
110
|
-
block.position = { x: Number(position.x ?? 0), y: Number(position.y ?? 0) };
|
|
111
|
-
}
|
|
112
|
-
/**
|
|
113
|
-
* PAGE-010 boşluk analizi patch'in SONUCUNU görmek zorunda: tek başına op'lardaki y değerleri,
|
|
114
|
-
* kapsayıcının mevcut çocukları bilinmeden anlamsız. Op'lar okunan şemanın kopyasına uygulanır
|
|
115
|
-
* (add: parent'ın children'ına ekle — kökler retargetRootAdds ile page_container'a gittiği için
|
|
116
|
-
* burada da öyle çözülür; move: düğümü taşı — parent verilmemişse aynı ebeveynde kalır — ve verilen
|
|
117
|
-
* position'ı desktop block'una yaz; ikisinde de index platformdaki gibi diziye yerleşir) ve analiz
|
|
118
|
-
* yalnız dokunulan kapsayıcılarda koşar.
|
|
119
|
-
*/
|
|
120
|
-
function simulateLayoutOps(schema, ops) {
|
|
121
|
-
const touched = new Set();
|
|
122
|
-
const attach = (node, requestedParent, index) => {
|
|
123
|
-
const parentId = typeof requestedParent === 'string' && requestedParent !== '' && requestedParent !== 'root'
|
|
124
|
-
? requestedParent
|
|
125
|
-
: PAGE_CONTAINER_ELEMENT_ID;
|
|
126
|
-
const parent = findNode(schema, parentId);
|
|
127
|
-
const siblings = parent ? [...(parent.children ?? [])] : schema;
|
|
128
|
-
const insertAt = typeof index === 'number' && Number.isFinite(index) ? Math.max(0, Math.min(index, siblings.length)) : siblings.length;
|
|
129
|
-
siblings.splice(insertAt, 0, node);
|
|
130
|
-
if (parent) {
|
|
131
|
-
parent.children = siblings;
|
|
132
|
-
touched.add(String(parent.elementId ?? parentId));
|
|
133
|
-
}
|
|
134
|
-
else {
|
|
135
|
-
touched.add('kök');
|
|
136
|
-
}
|
|
137
|
-
};
|
|
138
|
-
for (const op of ops) {
|
|
139
|
-
if (op.op === 'add_component' && op.component && typeof op.component === 'object') {
|
|
140
|
-
attach(JSON.parse(JSON.stringify(op.component)), op.parent, op.index);
|
|
141
|
-
}
|
|
142
|
-
// Düzen prop'ları (layout/flex grubu) simülasyona girer: PAGE-010 flex kapsayıcının içini
|
|
143
|
-
// analiz etmez, PAGE-015 ise op'tan SONRAKİ layout değerine bakar — ikisi de op uygulanmadan
|
|
144
|
-
// doğru karar veremez.
|
|
145
|
-
if (op.op === 'replace_prop' && FLEX_LAYOUT_PROP_NAMES.has(String(op.prop ?? ''))) {
|
|
146
|
-
const target = findNode(schema, String(op.target ?? ''));
|
|
147
|
-
const codeWrapper = op.value && typeof op.value === 'object' && op.value.type === 'code';
|
|
148
|
-
if (target)
|
|
149
|
-
target.props = { ...(target.props ?? {}), [String(op.prop)]: codeWrapper ? op.value : { type: 'static', ref: null, data: op.value } };
|
|
150
|
-
}
|
|
151
|
-
if (op.op === 'move_component') {
|
|
152
|
-
const moved = removeNodeFromTree(schema, String(op.target ?? ''));
|
|
153
|
-
if (!moved)
|
|
154
|
-
continue;
|
|
155
|
-
if (op.position && typeof op.position === 'object')
|
|
156
|
-
setDesktopPosition(moved.node, op.position);
|
|
157
|
-
attach(moved.node, op.parent ?? moved.parent?.elementId ?? 'root', op.index);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
return touched;
|
|
161
|
-
}
|
|
162
41
|
/**
|
|
163
42
|
* G-10: `index` taşıyan add/move op'u varsa sayfa okunur ve `flexIndexOrder` flex kapta görünen
|
|
164
43
|
* sırayı konumlara çevirir. Okuma başarısız olursa op'lar olduğu gibi gider — bugünkü davranış.
|
|
@@ -179,151 +58,16 @@ async function fetchPage(platform, project, environment, applicationId, pageId)
|
|
|
179
58
|
return response?.data ?? {};
|
|
180
59
|
}
|
|
181
60
|
/**
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
* `target: 'page'` sayfa düzeyi olayları hedefler.
|
|
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.
|
|
187
65
|
*/
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
for (const op of ops) {
|
|
194
|
-
const target = String(op.target ?? '');
|
|
195
|
-
const event = String(op.event ?? '');
|
|
196
|
-
if (!target || !event)
|
|
197
|
-
throw new PlatformApiError("remove_event için 'target' ve 'event' zorunludur.", 400);
|
|
198
|
-
if (target === 'page') {
|
|
199
|
-
const before = pageEvents.length;
|
|
200
|
-
const kept = pageEvents.filter(entry => String(entry?.name ?? '') !== event);
|
|
201
|
-
if (kept.length !== before) {
|
|
202
|
-
pageEvents.length = 0;
|
|
203
|
-
pageEvents.push(...kept);
|
|
204
|
-
pageEventsTouched = true;
|
|
205
|
-
removed.push(`page.${event}`);
|
|
206
|
-
}
|
|
207
|
-
continue;
|
|
208
|
-
}
|
|
209
|
-
const node = findNode(schema, target);
|
|
210
|
-
if (!node)
|
|
211
|
-
throw new PlatformApiError(`remove_event hedefi bulunamadı: '${target}'. page_schema_get ile elementId'leri listeleyin.`, 400);
|
|
212
|
-
const events = Array.isArray(node.events) ? node.events : [];
|
|
213
|
-
const kept = events.filter(entry => String(entry?.name ?? '') !== event);
|
|
214
|
-
if (kept.length !== events.length)
|
|
215
|
-
removed.push(`${target}.${event}`);
|
|
216
|
-
node.events = kept;
|
|
217
|
-
}
|
|
218
|
-
if (removed.length === 0)
|
|
219
|
-
return [];
|
|
220
|
-
const body = pageUpdateBody(page, pageEventsTouched ? { schema, events: pageEvents } : { schema });
|
|
221
|
-
await platform.tenant('PUT', project, `/pages/${applicationId}/${pageId}`, { environment, body });
|
|
222
|
-
return removed;
|
|
223
|
-
}
|
|
224
|
-
function findParentNode(nodes, target) {
|
|
225
|
-
for (const node of nodes) {
|
|
226
|
-
if ((node.children ?? []).some(child => child.elementId === target))
|
|
227
|
-
return node;
|
|
228
|
-
const deeper = findParentNode(node.children ?? [], target);
|
|
229
|
-
if (deeper)
|
|
230
|
-
return deeper;
|
|
231
|
-
}
|
|
232
|
-
return undefined;
|
|
233
|
-
}
|
|
234
|
-
/**
|
|
235
|
-
* `resize_component` gateway emülasyonudur: platformun micro-op sözlüğünde boyut YOKTUR
|
|
236
|
-
* (`move_component` yalnız `position` uygular, gövdeye konan `size` 200 döner ve sessizce düşer —
|
|
237
|
-
* ölçüldü 09-04). Bu yüzden kurulmuş bir sayfada bir kartın yüksekliğini değiştirmenin tek yolu
|
|
238
|
-
* düğümü `remove_component` + `add_component` ile yeniden kurmaktı; o da ona yazan akış
|
|
239
|
-
* setter'larını sessizce öldürüyordu (FLOW-018). Op şemayı okur, hedefin boyutunu değiştirir ve
|
|
240
|
-
* sayfayı tam gövdeyle geri yazar — `remove_event` ile aynı desen.
|
|
241
|
-
*
|
|
242
|
-
* Izgarada boyut `blocks[].size`, flex'te `appearance` alanlarındadır; ikisi de kabul edilir ve
|
|
243
|
-
* ebeveynin düzeni okunanı etkisiz kılıyorsa uyarı üretilir (PAGE-015 ile aynı yaklaşım: reddetme,
|
|
244
|
-
* ama sessiz de kalma).
|
|
245
|
-
*/
|
|
246
|
-
/**
|
|
247
|
-
* G-25 — `states` bir prop değil düğüm alanı: `replace_prop` yazamıyor, `move_component` yalnız
|
|
248
|
-
* konum uyguluyor ve `resize_component` (G-14) boyutla sınırlıydı. Kurulmuş bir düğümün
|
|
249
|
-
* `isWrapper` bayrağını değiştirmenin tek yolu düğümü silip yeniden kurmaktı; 09-05 koşusunda
|
|
250
|
-
* bunun bedeli 20 setter'ın sessizce kopmasıydı (G-21). Anahtar kümesi beyaz listeyle sınırlı:
|
|
251
|
-
* `states` açık uçlu bir sözlük ve uydurma bayrak sessizce kaydedilirdi.
|
|
252
|
-
*/
|
|
253
|
-
const WRITABLE_STATES = new Set(['isWrapper', 'isScrollable', 'isSticky', 'isDroppable', 'isFloating', 'isQuickStack']);
|
|
254
|
-
async function applyResizes(platform, project, environment, applicationId, pageId, page, ops) {
|
|
255
|
-
const schema = JSON.parse(JSON.stringify(page.schema ?? []));
|
|
256
|
-
const applied = [];
|
|
257
|
-
const warnings = [];
|
|
258
|
-
for (const op of ops) {
|
|
259
|
-
const target = String(op.target ?? '');
|
|
260
|
-
const size = op.size && typeof op.size === 'object' ? op.size : undefined;
|
|
261
|
-
const appearance = op.appearance && typeof op.appearance === 'object' ? op.appearance : undefined;
|
|
262
|
-
const states = op.states && typeof op.states === 'object' ? op.states : undefined;
|
|
263
|
-
const breakpoint = Number(op.breakpoint ?? 1024);
|
|
264
|
-
if (!target)
|
|
265
|
-
throw new PlatformApiError("resize_component için 'target' zorunludur.", 400);
|
|
266
|
-
if (!size && !appearance && !states) {
|
|
267
|
-
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);
|
|
268
|
-
}
|
|
269
|
-
if (states) {
|
|
270
|
-
const unknownStates = Object.keys(states).filter(key => !WRITABLE_STATES.has(key));
|
|
271
|
-
if (unknownStates.length > 0) {
|
|
272
|
-
throw new PlatformApiError(`resize_component '${target}': bilinmeyen states anahtarı (${unknownStates.join(', ')}). Yazılabilir bayraklar: ${[...WRITABLE_STATES].join(', ')}.`, 400);
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
const node = findNode(schema, target);
|
|
276
|
-
if (!node)
|
|
277
|
-
throw new PlatformApiError(`resize_component hedefi bulunamadı: '${target}'. page_schema_get ile elementId'leri listeleyin.`, 400);
|
|
278
|
-
const parent = findParentNode(schema, target);
|
|
279
|
-
const inFlex = parent !== undefined && isFlexContainer(parent);
|
|
280
|
-
if (size) {
|
|
281
|
-
const blocks = Array.isArray(node.blocks) ? node.blocks : [];
|
|
282
|
-
const block = blocks.find(entry => Number(entry?.breakpoint) === breakpoint);
|
|
283
|
-
if (!block) {
|
|
284
|
-
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);
|
|
285
|
-
}
|
|
286
|
-
const current = (block.size ?? {});
|
|
287
|
-
block.size = { w: Number(size.w ?? current.w ?? 0), h: Number(size.h ?? current.h ?? 0) };
|
|
288
|
-
applied.push(`${target}.size=${JSON.stringify(block.size)}`);
|
|
289
|
-
if (inFlex) {
|
|
290
|
-
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.`);
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
if (states) {
|
|
294
|
-
node.states = { ...(node.states ?? {}), ...states };
|
|
295
|
-
applied.push(`${target}.states=${JSON.stringify(states)}`);
|
|
296
|
-
if (states.isWrapper === true) {
|
|
297
|
-
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.`);
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
if (appearance) {
|
|
301
|
-
node.appearance = { ...(node.appearance ?? {}), ...appearance };
|
|
302
|
-
applied.push(`${target}.appearance=${JSON.stringify(appearance)}`);
|
|
303
|
-
if (!inFlex && parent !== undefined) {
|
|
304
|
-
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.`);
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
if (applied.length === 0)
|
|
309
|
-
return { applied, warnings };
|
|
310
|
-
const body = pageUpdateBody(page, { schema });
|
|
311
|
-
await platform.tenant('PUT', project, `/pages/${applicationId}/${pageId}`, { environment, body });
|
|
312
|
-
return { applied, warnings };
|
|
313
|
-
}
|
|
314
|
-
async function applyPropTypes(platform, project, environment, applicationId, pageId, page, ops) {
|
|
315
|
-
const schema = JSON.parse(JSON.stringify(page.schema ?? []));
|
|
316
|
-
const { applied, missingTargets } = applyPropTypesToSchema(schema, ops);
|
|
317
|
-
if (missingTargets.length > 0) {
|
|
318
|
-
throw new PlatformApiError(`set_prop_type hedefi bulunamadı: '${missingTargets[0]}'. page_schema_get ile elementId'leri listeleyin.`, 400);
|
|
319
|
-
}
|
|
320
|
-
if (applied.length === 0)
|
|
321
|
-
return [];
|
|
322
|
-
const body = pageUpdateBody(page, { schema });
|
|
323
|
-
await platform.tenant('PUT', project, `/pages/${applicationId}/${pageId}`, { environment, body });
|
|
324
|
-
return applied;
|
|
325
|
-
}
|
|
326
|
-
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) {
|
|
327
71
|
const config = platform.config;
|
|
328
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.");
|
|
329
73
|
/** Sayfa ve uç araçları aynı kimlik sözleşmesini paylaşır (G-20); çözüm tokenVault'ta yaşar. */
|
|
@@ -434,12 +178,23 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
|
|
|
434
178
|
appFunctions: runtimeFunctions(body.appFunctions),
|
|
435
179
|
pageFunctions: runtimeFunctions(body.pageFunctions),
|
|
436
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
|
+
: {}),
|
|
437
191
|
note: resolveStyles
|
|
438
|
-
? "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."
|
|
439
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."
|
|
440
194
|
};
|
|
441
195
|
}));
|
|
442
196
|
const screenshotter = injectedScreenshotter ?? new StudioScreenshotter(config);
|
|
197
|
+
const captureMemory = injectedMemory ?? sharedCaptureMemory;
|
|
443
198
|
const maxInlinePngBytes = 5 * 1024 * 1024;
|
|
444
199
|
server.registerTool('page_screenshot', {
|
|
445
200
|
title: 'Sayfanın ekran görüntüsünü al',
|
|
@@ -462,22 +217,38 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
|
|
|
462
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."),
|
|
463
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."),
|
|
464
219
|
actions: z.array(z.object({
|
|
465
|
-
action: z.enum(['set', 'click', 'wait', 'drag']),
|
|
466
|
-
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')"),
|
|
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"),
|
|
467
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>'"),
|
|
468
|
-
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'"),
|
|
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"),
|
|
469
224
|
ms: z.number().int().min(0).max(15000).optional().describe("'wait' için bekleme süresi")
|
|
470
|
-
})).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:'drag', target:'kb_board__card-42', to:'kb_board__col-Assigned'}]. '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)."),
|
|
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)."),
|
|
471
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ı."),
|
|
472
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."),
|
|
473
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')
|
|
474
239
|
},
|
|
475
240
|
annotations: readOnly
|
|
476
|
-
}, 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 }) => {
|
|
477
242
|
try {
|
|
478
243
|
if (anonymous === true && (asUser !== undefined || cookies !== undefined)) {
|
|
479
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.");
|
|
480
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
|
+
}
|
|
481
252
|
const incompleteDrag = actions?.findIndex(step => step.action === 'drag' && (step.target === undefined || step.to === undefined)) ?? -1;
|
|
482
253
|
if (incompleteDrag >= 0) {
|
|
483
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'}.`);
|
|
@@ -514,11 +285,12 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
|
|
|
514
285
|
const size = viewport ?? { width: 1280, height: 800 };
|
|
515
286
|
const extraCookies = Object.entries(cookies ?? {}).map(([name, value]) => ({ name, value }));
|
|
516
287
|
const identity = anonymous === true ? 'anonymous' : asUser === undefined ? 'gateway' : 'asUser';
|
|
288
|
+
const identityToken = anonymous ? undefined : resolveIdentity(asUser) ?? await platform.accessToken();
|
|
517
289
|
const cookieNames = [...new Set([...(identity === 'anonymous' ? [] : ['NG_AccessToken']), ...extraCookies.map(cookie => cookie.name)])];
|
|
518
290
|
const shot = await screenshotter.capture({
|
|
519
291
|
url,
|
|
520
292
|
apiPathMarker: `/${applicationSlug}/p/${pageSlug}`,
|
|
521
|
-
token:
|
|
293
|
+
token: identityToken,
|
|
522
294
|
...(extraCookies.length > 0 ? { cookies: extraCookies } : {}),
|
|
523
295
|
cookieUrl: config.studioBaseUrl,
|
|
524
296
|
viewport: size,
|
|
@@ -527,8 +299,78 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
|
|
|
527
299
|
...(captureUnready === true ? { captureUnready: true } : {}),
|
|
528
300
|
...(layoutReport === true ? { layoutReport: true } : {}),
|
|
529
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 }),
|
|
530
310
|
...(actions === undefined ? {} : { actions: actions })
|
|
531
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.`);
|
|
532
374
|
const meta = {
|
|
533
375
|
url,
|
|
534
376
|
environment: resolvedEnvironment,
|
|
@@ -539,8 +381,41 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
|
|
|
539
381
|
fullPage: fullPage === true,
|
|
540
382
|
identity,
|
|
541
383
|
cookieNames,
|
|
542
|
-
|
|
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
|
+
: {}),
|
|
543
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
|
+
}),
|
|
544
419
|
...(shot.contentHeight === undefined ? {} : { contentHeight: shot.contentHeight }),
|
|
545
420
|
...(shot.fullPageTruncated
|
|
546
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.` }
|
|
@@ -593,16 +468,7 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
|
|
|
593
468
|
runtimeErrors: shot.runtimeErrors,
|
|
594
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.`
|
|
595
470
|
}),
|
|
596
|
-
...(
|
|
597
|
-
? { 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.` }
|
|
598
|
-
: {}),
|
|
599
|
-
...(shot.actions?.some(outcome => outcome.propWritten === false)
|
|
600
|
-
? { actionsNote: `Değer DOM'a yazıldı ama runtime prop'una ULAŞMADI: ${shot.actions.filter(outcome => outcome.propWritten === false).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.` }
|
|
601
|
-
: {}),
|
|
602
|
-
// 'wait' bir eylem değil bekleyiştir: akış sayacı ona bağlanmaz, notu tetiklemez (33/S-1).
|
|
603
|
-
...(shot.actions?.some(outcome => outcome.action !== 'wait' && outcome.applied && outcome.requests === 0 && (outcome.flowRuns ?? 0) === 0)
|
|
604
|
-
? { 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.` }
|
|
605
|
-
: {}),
|
|
471
|
+
...(actionNotes.length === 0 ? {} : { actionsNote: actionNotes.join('\n') }),
|
|
606
472
|
...(shot.layout === undefined
|
|
607
473
|
? {}
|
|
608
474
|
: shot.layout.length === 0
|
|
@@ -617,6 +483,8 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
|
|
|
617
483
|
try {
|
|
618
484
|
// B-6: klasör yoksa ENOENT ile düşüyordu; koşu ajanı her seferinde klasörü elle açıyordu.
|
|
619
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.');
|
|
620
488
|
await writeFile(absolute, shot.png);
|
|
621
489
|
}
|
|
622
490
|
catch (error) {
|
|
@@ -625,12 +493,19 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
|
|
|
625
493
|
}
|
|
626
494
|
return toolResult({ path: absolute, ...meta }, config);
|
|
627
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
|
+
}
|
|
628
503
|
if (shot.png.byteLength > maxInlinePngBytes) {
|
|
629
|
-
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.`);
|
|
630
505
|
}
|
|
631
506
|
return {
|
|
632
507
|
content: [
|
|
633
|
-
{ type: 'image', data: shot.png.toString('base64'), mimeType: 'image/png' },
|
|
508
|
+
{ type: 'image', data: shot.png.toString('base64'), mimeType: shot.mimeType ?? 'image/png' },
|
|
634
509
|
{ type: 'text', text: JSON.stringify(meta) }
|
|
635
510
|
],
|
|
636
511
|
structuredContent: meta
|
|
@@ -642,7 +517,7 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
|
|
|
642
517
|
});
|
|
643
518
|
server.registerTool('page_schema_get', {
|
|
644
519
|
title: 'Sayfa şemasını oku',
|
|
645
|
-
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.",
|
|
646
521
|
inputSchema: {
|
|
647
522
|
project: projectParam,
|
|
648
523
|
environment: environmentParam,
|
|
@@ -655,17 +530,18 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
|
|
|
655
530
|
}, handle(config, async ({ project, environment, applicationId, pageId, mode, componentId }) => {
|
|
656
531
|
const page = await fetchPage(platform, project, environment, applicationId, pageId);
|
|
657
532
|
const updatedAt = page.audit?.updatedAt ?? page.audit?.createdAt;
|
|
533
|
+
const version = page.version;
|
|
658
534
|
if (mode === 'full')
|
|
659
|
-
return { updatedAt, schema: page.schema ?? [] };
|
|
535
|
+
return { version, updatedAt, schema: page.schema ?? [] };
|
|
660
536
|
if (mode === 'subtree') {
|
|
661
537
|
if (!componentId)
|
|
662
538
|
throw new PlatformApiError("mode='subtree' için componentId zorunludur.");
|
|
663
539
|
const subtree = findNode(page.schema, componentId);
|
|
664
540
|
if (!subtree)
|
|
665
541
|
throw new PlatformApiError(`Sayfada '${componentId}' id'li bir komponent yok; page_schema_get'i summary kipinde çağırıp hedefi doğrulayın.`);
|
|
666
|
-
return { updatedAt, schema: subtree };
|
|
542
|
+
return { version, updatedAt, schema: subtree };
|
|
667
543
|
}
|
|
668
|
-
return { updatedAt, schema: (page.schema ?? []).map(node => summarize(node)) };
|
|
544
|
+
return { version, updatedAt, schema: (page.schema ?? []).map(node => summarize(node)) };
|
|
669
545
|
}));
|
|
670
546
|
server.registerTool('page_state', {
|
|
671
547
|
title: 'Sayfa durumunu getir',
|
|
@@ -683,9 +559,9 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
|
|
|
683
559
|
const propTypeIssues = setPropTypeErrors(allOps.filter(entry => entry.op === 'set_prop_type'));
|
|
684
560
|
if (propTypeIssues.length > 0)
|
|
685
561
|
throw new PlatformApiError(`set_prop_type gövdesi eksik (istek gönderilmedi): ${propTypeIssues.join(' · ')}`, 400);
|
|
686
|
-
//
|
|
687
|
-
//
|
|
688
|
-
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');
|
|
689
565
|
const normalized = normalizeSchemaOps(opRecords);
|
|
690
566
|
const validationIssues = [
|
|
691
567
|
...schemaOpValidationErrors(opRecords).map(issue => `PAGE-016 — ${issue}`),
|
|
@@ -694,7 +570,7 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
|
|
|
694
570
|
const ordering = await orderByIndex(opRecords, () => fetchPage(platform, project, environment, applicationId, pageId));
|
|
695
571
|
const response = ((await platform.tenant('POST', project, `/pages/${applicationId}/${pageId}/schema/validate`, {
|
|
696
572
|
environment,
|
|
697
|
-
body: { ops: ordering.ops }
|
|
573
|
+
body: { ops: [...allOps.filter(entry => entry.op === 'resize_component').flatMap(resizeServerOps), ...ordering.ops] }
|
|
698
574
|
})) ?? {});
|
|
699
575
|
if (validationIssues.length > 0)
|
|
700
576
|
response.validationWarnings = validationIssues;
|
|
@@ -709,7 +585,7 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
|
|
|
709
585
|
}));
|
|
710
586
|
server.registerTool('page_schema_patch', {
|
|
711
587
|
title: 'Sayfa şemasına patch uygula',
|
|
712
|
-
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.",
|
|
713
589
|
inputSchema: {
|
|
714
590
|
project: projectParam,
|
|
715
591
|
environment: environmentParam,
|
|
@@ -717,12 +593,13 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
|
|
|
717
593
|
pageId: guid,
|
|
718
594
|
ops: opsParam.optional(),
|
|
719
595
|
operations: opsParam.optional().describe("'ops' ile aynı alan (eşanlamlı) — entity/microservice/workflow patch'leriyle aynı adı kullanabilmeniz için"),
|
|
720
|
-
|
|
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"),
|
|
721
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."),
|
|
722
599
|
view: writeViewParam
|
|
723
600
|
},
|
|
724
601
|
annotations: updates
|
|
725
|
-
}, 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 }) => {
|
|
726
603
|
const allOps = microOps('page_schema_patch', ops, operations).map(op => op);
|
|
727
604
|
const removeEventOps = allOps.filter(entry => entry.op === 'remove_event');
|
|
728
605
|
const resizeOps = allOps.filter(entry => entry.op === 'resize_component');
|
|
@@ -787,7 +664,9 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
|
|
|
787
664
|
// Akış silen op'lar yamadan ÖNCE hesaplanır: uyarı sonradan geldiğinde akış zaten kalıcı
|
|
788
665
|
// silinmiş oluyordu ve "önce yedekle" tavsiyesi uygulanamıyordu (09-05 koşusu, G-21).
|
|
789
666
|
let deletedFlows = [];
|
|
790
|
-
|
|
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)))) {
|
|
791
670
|
try {
|
|
792
671
|
const page = await readPage();
|
|
793
672
|
deletedFlows = orphanedFlowPlan(page.schema, page.events, [...opRecords, ...removeEventOps]);
|
|
@@ -854,31 +733,25 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
|
|
|
854
733
|
}
|
|
855
734
|
if (nestedAfterOps.length > 0)
|
|
856
735
|
throw new PlatformApiError(nestedRepeaterMessage(nestedAfterOps), 400);
|
|
857
|
-
const removedEvents = removeEventOps
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
cachedPage = undefined;
|
|
862
|
-
const resized = resizeOps.length > 0
|
|
863
|
-
? await applyResizes(platform, project, environment, applicationId, pageId, await readPage(), resizeOps)
|
|
864
|
-
: { applied: [], warnings: [] };
|
|
865
|
-
if (resizeOps.length > 0)
|
|
866
|
-
cachedPage = undefined;
|
|
867
|
-
const propTypes = propTypeOps.length > 0
|
|
868
|
-
? await applyPropTypes(platform, project, environment, applicationId, pageId, await readPage(), propTypeOps)
|
|
869
|
-
: [];
|
|
870
|
-
if (propTypeOps.length > 0)
|
|
871
|
-
cachedPage = undefined;
|
|
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];
|
|
872
740
|
layoutWarnings = [...ordering.warnings, ...layoutWarnings];
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
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
|
+
})) ?? {});
|
|
876
749
|
if (view !== 'full')
|
|
877
750
|
outlineWriteResponse(response);
|
|
878
751
|
if (removedEvents.length > 0)
|
|
879
752
|
response.removedEvents = removedEvents;
|
|
880
|
-
if (propTypes.length > 0)
|
|
881
|
-
response.propTypes = propTypes;
|
|
753
|
+
if (propTypes.applied.length > 0)
|
|
754
|
+
response.propTypes = propTypes.applied;
|
|
882
755
|
if (resized.applied.length > 0)
|
|
883
756
|
response.resized = resized.applied;
|
|
884
757
|
if (resized.warnings.length > 0)
|