@appaflytech/zeyrek-ai-mcp 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +46 -0
- package/README.md +260 -0
- package/dist/config.js +128 -0
- package/dist/index.js +88 -0
- package/dist/knowledge/componentCatalog.js +438 -0
- package/dist/knowledge/componentEvents.js +164 -0
- package/dist/knowledge/coverage.js +60 -0
- package/dist/knowledge/normalize.js +1309 -0
- package/dist/knowledge/outlines.js +167 -0
- package/dist/knowledge/policy.js +44 -0
- package/dist/knowledge/reservedTables.js +75 -0
- package/dist/knowledge/rules.js +1725 -0
- package/dist/observability/trace.js +337 -0
- package/dist/platform/errors.js +132 -0
- package/dist/platform/http.js +169 -0
- package/dist/platform/responses.js +94 -0
- package/dist/platform/token.js +79 -0
- package/dist/resources/examples/entity-pair-m2m.json +105 -0
- package/dist/resources/examples/flow-list-to-form.json +65 -0
- package/dist/resources/examples/flow-load-table.json +178 -0
- package/dist/resources/examples/microservice-table-list.json +49 -0
- package/dist/resources/examples/middleware-authenticate.json +194 -0
- package/dist/resources/examples/page-detail-profile.json +95 -0
- package/dist/resources/examples/page-dynamic-form.json +221 -0
- package/dist/resources/examples/page-form-upsert.json +279 -0
- package/dist/resources/examples/page-html-embed.json +106 -0
- package/dist/resources/examples/page-sign-in.json +194 -0
- package/dist/resources/examples/workflow-basic.json +94 -0
- package/dist/resources/guides/component.md +108 -0
- package/dist/resources/guides/entity.md +348 -0
- package/dist/resources/guides/expressions.md +139 -0
- package/dist/resources/guides/getting-started.md +109 -0
- package/dist/resources/guides/html.md +106 -0
- package/dist/resources/guides/microservice.md +313 -0
- package/dist/resources/guides/middleware.md +271 -0
- package/dist/resources/guides/modify-existing.md +35 -0
- package/dist/resources/guides/page.md +1574 -0
- package/dist/resources/guides/view.md +89 -0
- package/dist/resources/guides/workflow.md +359 -0
- package/dist/resources/knowledge.js +44 -0
- package/dist/resources/schemas/CreateDatasourceCommand.json +54 -0
- package/dist/resources/schemas/CreateDiscoveredEntitiesCommand.json +133 -0
- package/dist/resources/schemas/CreateEntityCommand.json +387 -0
- package/dist/resources/schemas/CreateMicroserviceCommand.json +528 -0
- package/dist/resources/schemas/CreateQueryCommand.json +43 -0
- package/dist/resources/schemas/CreateViewCommand.json +158 -0
- package/dist/resources/schemas/CreateWorkflowCommand.json +1719 -0
- package/dist/resources/schemas/Filter.json +124 -0
- package/dist/resources/schemas/GetRowsQuery.json +122 -0
- package/dist/resources/schemas/PreviewMicroserviceCommand.json +565 -0
- package/dist/resources/schemas/PreviewQueryCommand.json +74 -0
- package/dist/resources/schemas/PreviewWorkflowCommand.json +981 -0
- package/dist/resources/schemas/StepDto.json +900 -0
- package/dist/resources/schemas/UpdateDatasourceCommand.json +58 -0
- package/dist/resources/schemas/UpdateEntityCommand.json +679 -0
- package/dist/resources/schemas/UpdateMicroserviceCommand.json +512 -0
- package/dist/resources/schemas/UpdateQueryCommand.json +86 -0
- package/dist/resources/schemas/UpdateViewCommand.json +153 -0
- package/dist/resources/schemas/UpdateWorkflowCommand.json +1719 -0
- package/dist/resources/schemas.js +40 -0
- package/dist/server.js +63 -0
- package/dist/tools/access.js +27 -0
- package/dist/tools/applications.js +59 -0
- package/dist/tools/automations.js +103 -0
- package/dist/tools/common.js +18 -0
- package/dist/tools/components.js +280 -0
- package/dist/tools/datasources.js +82 -0
- package/dist/tools/dependencies.js +17 -0
- package/dist/tools/deployments.js +17 -0
- package/dist/tools/dtos.js +54 -0
- package/dist/tools/entities.js +166 -0
- package/dist/tools/files.js +138 -0
- package/dist/tools/flows.js +226 -0
- package/dist/tools/folders.js +51 -0
- package/dist/tools/layouts.js +105 -0
- package/dist/tools/microservices.js +198 -0
- package/dist/tools/organizations.js +65 -0
- package/dist/tools/pages.js +597 -0
- package/dist/tools/platformInfo.js +44 -0
- package/dist/tools/projects.js +152 -0
- package/dist/tools/queries.js +48 -0
- package/dist/tools/rows.js +115 -0
- package/dist/tools/screens.js +97 -0
- package/dist/tools/templates.js +65 -0
- package/dist/tools/variables.js +94 -0
- package/dist/tools/views.js +58 -0
- package/dist/tools/workflows.js +196 -0
- package/package.json +49 -0
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
import { guid, projectParam, environmentParam, pagingParams, readOnly, writes, updates, destructive } from './common.js';
|
|
2
|
+
import { orphanedFlowWarnings, normalizeComponentDefaults, normalizeSchemaOps, schemaOpValidationErrors, validationNote, componentDefaultsNote, replacePropNote, ensurePageContainer, retargetRootAdds, rootAddsNote, blockSizeNote, heightTypeNote, reorderSchemaBlocks, reorderNote, nestedRepeaterNodes, nestedRepeaterMessage, stringStyleNodes, stringStyleWarning, inertFlexPropNodes, inertFlexPropWarning, FLEX_LAYOUT_PROP_NAMES, resetPageContainerPosition, pageContainerPositionWarning, verticalGapWarnings, invalidScreenEventNames, invalidWidgetTypeNodes, withNormalization, PAGE_CONTAINER_ELEMENT_ID } from '../knowledge/normalize.js';
|
|
3
|
+
import { fetchHost, pageUpdateBody, schemaParam, eventsParam, unwrap } from './screens.js';
|
|
4
|
+
import { applyCatalogLayoutRules, fetchComponentEventNames } from './components.js';
|
|
5
|
+
import { formBindingWarnings } from '../knowledge/coverage.js';
|
|
6
|
+
import { PlatformApiError } from '../platform/errors.js';
|
|
7
|
+
import { handle } from '../platform/responses.js';
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
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). target = elementId.");
|
|
10
|
+
const modeParam = z.enum(['summary', 'subtree', 'full']).optional().describe("summary (varsayılan): yalnız id/elementId/name/type ağacı — hedef bulmak için bunu kullanın. subtree: componentId ile tek alt ağaç. full: tüm şema.");
|
|
11
|
+
/**
|
|
12
|
+
* PAGE-008 — `set_event` uydurma bir olay adıyla 200 döner ve bağ SESSİZCE ölü kalır: motor olayı
|
|
13
|
+
* komponentin yaydığı adla eşleştirir, eşleşme yoksa akış hiç çalışmaz. Kontrol yalnız hedefin
|
|
14
|
+
* tipini ve o tipin katalogdaki olay listesini pozitif olarak çözebildiğinde konuşur (layout/widget
|
|
15
|
+
* içindeki hedefler ve katalogda olay tanımlamayan tipler doğrulanmadan geçer).
|
|
16
|
+
*/
|
|
17
|
+
function unknownEventOps(ops, typesByElementId, eventsBySlug) {
|
|
18
|
+
return ops
|
|
19
|
+
.map(op => op)
|
|
20
|
+
.filter(op => op.op === 'set_event')
|
|
21
|
+
.flatMap(op => {
|
|
22
|
+
const target = String(op.target ?? '');
|
|
23
|
+
const event = String(op.event ?? '');
|
|
24
|
+
const type = typesByElementId.get(target);
|
|
25
|
+
const declared = type ? eventsBySlug.get(type) : undefined;
|
|
26
|
+
if (!event || !declared || declared.includes(event))
|
|
27
|
+
return [];
|
|
28
|
+
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.`];
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
function collectTypes(nodes, into) {
|
|
32
|
+
for (const node of nodes ?? []) {
|
|
33
|
+
if (typeof node.elementId === 'string' && typeof node.type === 'string')
|
|
34
|
+
into.set(node.elementId, node.type);
|
|
35
|
+
collectTypes(node.children, into);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function invalidComponentNames(nodes) {
|
|
39
|
+
const found = [];
|
|
40
|
+
for (const node of nodes ?? []) {
|
|
41
|
+
const name = node.name;
|
|
42
|
+
if (typeof name === 'string' && name.length > 0 && !/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name.replace(/-/g, ''))) {
|
|
43
|
+
found.push(`'${name}' (${node.elementId ?? '?'})`);
|
|
44
|
+
}
|
|
45
|
+
found.push(...invalidComponentNames(node.children));
|
|
46
|
+
}
|
|
47
|
+
return found;
|
|
48
|
+
}
|
|
49
|
+
function summarize(node) {
|
|
50
|
+
const summary = {
|
|
51
|
+
id: node.id,
|
|
52
|
+
elementId: node.elementId,
|
|
53
|
+
name: node.name,
|
|
54
|
+
type: node.type
|
|
55
|
+
};
|
|
56
|
+
if (Array.isArray(node.children) && node.children.length > 0) {
|
|
57
|
+
summary.children = node.children.map(summarize);
|
|
58
|
+
}
|
|
59
|
+
return summary;
|
|
60
|
+
}
|
|
61
|
+
function findNode(nodes, target) {
|
|
62
|
+
for (const node of nodes ?? []) {
|
|
63
|
+
if (node.elementId === target || String(node.id) === target)
|
|
64
|
+
return node;
|
|
65
|
+
const found = findNode(node.children, target);
|
|
66
|
+
if (found)
|
|
67
|
+
return found;
|
|
68
|
+
}
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
function removeNodeFromTree(nodes, target) {
|
|
72
|
+
for (let index = 0; index < nodes.length; index++) {
|
|
73
|
+
if (nodes[index].elementId === target)
|
|
74
|
+
return nodes.splice(index, 1)[0];
|
|
75
|
+
const found = removeNodeFromTree(nodes[index].children ?? [], target);
|
|
76
|
+
if (found)
|
|
77
|
+
return found;
|
|
78
|
+
}
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
function setDesktopPosition(node, position) {
|
|
82
|
+
const blocks = Array.isArray(node.blocks) ? node.blocks : [];
|
|
83
|
+
const block = blocks.find(entry => Number(entry?.breakpoint) === 1024);
|
|
84
|
+
if (block)
|
|
85
|
+
block.position = { x: Number(position.x ?? 0), y: Number(position.y ?? 0) };
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* PAGE-010 boşluk analizi patch'in SONUCUNU görmek zorunda: tek başına op'lardaki y değerleri,
|
|
89
|
+
* kapsayıcının mevcut çocukları bilinmeden anlamsız. Op'lar okunan şemanın kopyasına uygulanır
|
|
90
|
+
* (add: parent'ın children'ına ekle — kökler retargetRootAdds ile page_container'a gittiği için
|
|
91
|
+
* burada da öyle çözülür; move: düğümü taşı ve verilen position'ı desktop block'una yaz) ve analiz
|
|
92
|
+
* yalnız dokunulan kapsayıcılarda koşar.
|
|
93
|
+
*/
|
|
94
|
+
function simulateLayoutOps(schema, ops) {
|
|
95
|
+
const touched = new Set();
|
|
96
|
+
const attach = (node, requestedParent) => {
|
|
97
|
+
const parentId = typeof requestedParent === 'string' && requestedParent !== '' && requestedParent !== 'root'
|
|
98
|
+
? requestedParent
|
|
99
|
+
: PAGE_CONTAINER_ELEMENT_ID;
|
|
100
|
+
const parent = findNode(schema, parentId);
|
|
101
|
+
if (parent) {
|
|
102
|
+
parent.children = [...(parent.children ?? []), node];
|
|
103
|
+
touched.add(String(parent.elementId ?? parentId));
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
schema.push(node);
|
|
107
|
+
touched.add('kök');
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
for (const op of ops) {
|
|
111
|
+
if (op.op === 'add_component' && op.component && typeof op.component === 'object') {
|
|
112
|
+
attach(JSON.parse(JSON.stringify(op.component)), op.parent);
|
|
113
|
+
}
|
|
114
|
+
// Düzen prop'ları (layout/flex grubu) simülasyona girer: PAGE-010 flex kapsayıcının içini
|
|
115
|
+
// analiz etmez, PAGE-015 ise op'tan SONRAKİ layout değerine bakar — ikisi de op uygulanmadan
|
|
116
|
+
// doğru karar veremez.
|
|
117
|
+
if (op.op === 'replace_prop' && FLEX_LAYOUT_PROP_NAMES.has(String(op.prop ?? ''))) {
|
|
118
|
+
const target = findNode(schema, String(op.target ?? ''));
|
|
119
|
+
if (target)
|
|
120
|
+
target.props = { ...(target.props ?? {}), [String(op.prop)]: { type: 'static', ref: null, data: op.value } };
|
|
121
|
+
}
|
|
122
|
+
if (op.op === 'move_component') {
|
|
123
|
+
const moved = removeNodeFromTree(schema, String(op.target ?? ''));
|
|
124
|
+
if (!moved)
|
|
125
|
+
continue;
|
|
126
|
+
if (op.position && typeof op.position === 'object')
|
|
127
|
+
setDesktopPosition(moved, op.position);
|
|
128
|
+
attach(moved, op.parent);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return touched;
|
|
132
|
+
}
|
|
133
|
+
async function fetchPage(platform, project, environment, applicationId, pageId) {
|
|
134
|
+
const response = await platform.tenant('GET', project, `/pages/${applicationId}/${pageId}`, { environment });
|
|
135
|
+
return response?.data ?? {};
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* F-8 — platformun `set_event` micro-op'u olay KALDIRAMAZ: `eventValue: null` verildiğinde
|
|
139
|
+
* PageSchemaMicroOpApplier kaydı `{ name: 'click' }` olarak (değersiz) bırakır, diziden çıkarmaz;
|
|
140
|
+
* ölü bağ şemada artık olarak kalır. Kaldırma yolu tek: şemayı okuyup diziden çıkarmak ve sayfayı
|
|
141
|
+
* tam gövdeyle geri yazmak (page_update'in kullandığı PUT — pageUpdateBody her alanı korur).
|
|
142
|
+
* `target: 'page'` sayfa düzeyi olayları hedefler.
|
|
143
|
+
*/
|
|
144
|
+
async function applyEventRemovals(platform, project, environment, applicationId, pageId, page, ops) {
|
|
145
|
+
const schema = JSON.parse(JSON.stringify(page.schema ?? []));
|
|
146
|
+
const pageEvents = Array.isArray(page.events) ? [...page.events] : [];
|
|
147
|
+
const removed = [];
|
|
148
|
+
let pageEventsTouched = false;
|
|
149
|
+
for (const op of ops) {
|
|
150
|
+
const target = String(op.target ?? '');
|
|
151
|
+
const event = String(op.event ?? '');
|
|
152
|
+
if (!target || !event)
|
|
153
|
+
throw new PlatformApiError("remove_event için 'target' ve 'event' zorunludur.", 400);
|
|
154
|
+
if (target === 'page') {
|
|
155
|
+
const before = pageEvents.length;
|
|
156
|
+
const kept = pageEvents.filter(entry => String(entry?.name ?? '') !== event);
|
|
157
|
+
if (kept.length !== before) {
|
|
158
|
+
pageEvents.length = 0;
|
|
159
|
+
pageEvents.push(...kept);
|
|
160
|
+
pageEventsTouched = true;
|
|
161
|
+
removed.push(`page.${event}`);
|
|
162
|
+
}
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const node = findNode(schema, target);
|
|
166
|
+
if (!node)
|
|
167
|
+
throw new PlatformApiError(`remove_event hedefi bulunamadı: '${target}'. page_schema_get ile elementId'leri listeleyin.`, 400);
|
|
168
|
+
const events = Array.isArray(node.events) ? node.events : [];
|
|
169
|
+
const kept = events.filter(entry => String(entry?.name ?? '') !== event);
|
|
170
|
+
if (kept.length !== events.length)
|
|
171
|
+
removed.push(`${target}.${event}`);
|
|
172
|
+
node.events = kept;
|
|
173
|
+
}
|
|
174
|
+
if (removed.length === 0)
|
|
175
|
+
return [];
|
|
176
|
+
const body = pageUpdateBody(page, pageEventsTouched ? { schema, events: pageEvents } : { schema });
|
|
177
|
+
await platform.tenant('PUT', project, `/pages/${applicationId}/${pageId}`, { environment, body });
|
|
178
|
+
return removed;
|
|
179
|
+
}
|
|
180
|
+
export function registerPageTools(server, platform) {
|
|
181
|
+
const config = platform.config;
|
|
182
|
+
server.registerTool('page_list', {
|
|
183
|
+
title: 'Sayfaları listele',
|
|
184
|
+
description: 'Uygulamanın sayfalarını (id, name, slug) listeler; hedef sayfayı bulmak için ilk adım.',
|
|
185
|
+
inputSchema: { project: projectParam, environment: environmentParam, applicationId: guid, ...pagingParams },
|
|
186
|
+
annotations: readOnly
|
|
187
|
+
}, handle(config, async ({ project, environment, applicationId, pageIndex, pageLength, order, isDescending }) => {
|
|
188
|
+
const response = await platform.tenant('GET', project, `/pages/${applicationId}`, {
|
|
189
|
+
environment,
|
|
190
|
+
query: { pageIndex: pageIndex ?? 0, pageLength: pageLength ?? 200, order, isDescending }
|
|
191
|
+
});
|
|
192
|
+
return {
|
|
193
|
+
pages: (response?.data ?? []).map(page => ({
|
|
194
|
+
id: page.id,
|
|
195
|
+
name: page.name,
|
|
196
|
+
slug: page.slug,
|
|
197
|
+
isHomepage: page.isHomepage
|
|
198
|
+
}))
|
|
199
|
+
};
|
|
200
|
+
}));
|
|
201
|
+
server.registerTool('page_get', {
|
|
202
|
+
title: 'Sayfa üstbilgisini getir',
|
|
203
|
+
description: "Sayfanın kimlik ve bağ bilgisi: slug, browserTitle, layoutId, middlewareIds, roleIds, sayfa düzeyi olaylar (events[].value = flow id) ve flow özetleri. ŞEMA DÖNDÜRMEZ — şema için page_schema_get. 'Sayfa middleware'e bağlı mı, hangi roller görüyor, hangi olaya hangi akış bağlı' sorularının cevabı budur (Kural 8 denetimi).",
|
|
204
|
+
inputSchema: { project: projectParam, environment: environmentParam, applicationId: guid, pageId: guid },
|
|
205
|
+
annotations: readOnly
|
|
206
|
+
}, handle(config, async ({ project, environment, applicationId, pageId }) => {
|
|
207
|
+
const page = await fetchPage(platform, project, environment, applicationId, pageId);
|
|
208
|
+
const layout = (page.layout ?? {});
|
|
209
|
+
const countNodes = (nodes) => (nodes ?? []).reduce((total, node) => total + 1 + countNodes(node.children), 0);
|
|
210
|
+
return {
|
|
211
|
+
id: page.id,
|
|
212
|
+
name: page.name,
|
|
213
|
+
slug: page.slug,
|
|
214
|
+
browserTitle: page.browserTitle ?? '',
|
|
215
|
+
isHomepage: page.isHomepage === true,
|
|
216
|
+
layoutId: page.layoutId ?? layout.id ?? null,
|
|
217
|
+
layoutName: layout.name,
|
|
218
|
+
middlewareIds: page.middlewareIds ?? [],
|
|
219
|
+
roleIds: Array.isArray(page.roleIds) && page.roleIds.length > 0
|
|
220
|
+
? page.roleIds
|
|
221
|
+
: (Array.isArray(page.roles) ? page.roles.map(role => role.id) : []),
|
|
222
|
+
events: page.events ?? [],
|
|
223
|
+
flows: (Array.isArray(page.flows) ? page.flows : [])
|
|
224
|
+
.map(flow => ({ id: flow.id, name: flow.name, slug: flow.slug })),
|
|
225
|
+
schemaNodes: countNodes(page.schema),
|
|
226
|
+
updatedAt: page.audit?.updatedAt ?? page.audit?.createdAt
|
|
227
|
+
};
|
|
228
|
+
}));
|
|
229
|
+
server.registerTool('page_runtime_get', {
|
|
230
|
+
title: 'Sayfanın runtime çıktısını getir',
|
|
231
|
+
description: "Çıkan uygulamanın sayfa için aldığı GERÇEK paketi döner (GET /{project}/{application}/p/{slug} — sayfanın ham serve yolu budur; Studio preview ve runtime aynı ucu tüketir): sayfa şeması + layout views'ü + sayfa olayları + flow'lar + değişken anahtarları. Kurduğun sayfanın SON hâlini doğrulamanın yolu budur (layout-slot yerleşimi dahil — Kural 1f). DİKKAT: sayfaya bağlı middleware'ler bu çağrıda GERÇEKTEN koşar; middleware reddederse yanıt middlewareBlocked ile döner — korumalı sayfada bu beklenen davranıştır ve kendisi bir doğrulamadır.",
|
|
232
|
+
inputSchema: {
|
|
233
|
+
project: projectParam,
|
|
234
|
+
environment: environmentParam,
|
|
235
|
+
applicationSlug: z.string().min(1).describe("Uygulamanın slug'ı (application_list / project_overview)"),
|
|
236
|
+
pageSlug: z.string().min(1).describe("Sayfanın slug'ı (page_list)"),
|
|
237
|
+
view: z.enum(['summary', 'full']).optional().describe('summary (varsayılan): şema/layout ağaçları id/name/type özetine indirgenir. full: ham gövde — büyük olabilir'),
|
|
238
|
+
anonymous: z.boolean().optional().describe("true: istek HİÇBİR Authorization header'ı olmadan gider — 'korumalı sayfa tokensız ne döner, şema sızıyor mu?' sorusunun cevabı budur. Korumalı sayfada middlewareBlocked bekleyin.")
|
|
239
|
+
},
|
|
240
|
+
annotations: readOnly
|
|
241
|
+
}, handle(config, async ({ project, environment, applicationSlug, pageSlug, view, anonymous }) => {
|
|
242
|
+
let response;
|
|
243
|
+
try {
|
|
244
|
+
response = await platform.tenant('GET', project, `/${applicationSlug}/p/${pageSlug}`, { environment, anonymous });
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
if (error instanceof PlatformApiError && typeof error.status === 'number' && error.status >= 300) {
|
|
248
|
+
return {
|
|
249
|
+
middlewareBlocked: true,
|
|
250
|
+
status: error.status,
|
|
251
|
+
message: error.message,
|
|
252
|
+
note: "Sayfaya bağlı middleware isteği reddetti ya da kayıt bulunamadı. Korumalı sayfada kimliksiz/yetkisiz isteğin reddedilmesi BEKLENEN davranıştır. Sayfanın bağlarını page_get ile, middleware tarifini platform_guide('middleware') ile kontrol edin; slug'ları page_list/application_list doğrular."
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
throw error;
|
|
256
|
+
}
|
|
257
|
+
if (view === 'full')
|
|
258
|
+
return response;
|
|
259
|
+
const body = (response?.data ?? {});
|
|
260
|
+
const layout = body.layout;
|
|
261
|
+
const asNodes = (value) => (Array.isArray(value) ? value : []);
|
|
262
|
+
const named = (value) => (Array.isArray(value) ? value : []).map(entry => ({ id: entry.id, name: entry.name, slug: entry.slug }));
|
|
263
|
+
return {
|
|
264
|
+
page: { id: body.id, name: body.name, slug: body.slug, browserTitle: body.browserTitle, isHomepage: body.isHomepage },
|
|
265
|
+
events: body.events ?? [],
|
|
266
|
+
layout: layout ? { id: layout.id, name: layout.name, views: asNodes(layout.views).map(summarize) } : null,
|
|
267
|
+
schema: asNodes(body.schema).map(summarize),
|
|
268
|
+
widgets: named(body.widgets),
|
|
269
|
+
flows: named(body.flows),
|
|
270
|
+
variableKeys: Object.keys((body.variables ?? {})),
|
|
271
|
+
hasTheme: Boolean(body.project?.theme),
|
|
272
|
+
note: "schema, layout'taki slot'un İÇİNE yerleşerek çizilir (Kural 1f); tam gövde gerekirse view:'full'."
|
|
273
|
+
};
|
|
274
|
+
}));
|
|
275
|
+
server.registerTool('page_schema_get', {
|
|
276
|
+
title: 'Sayfa şemasını oku',
|
|
277
|
+
description: "Sayfanın komponent şemasını okur; yanıttaki updatedAt değeri page_schema_patch'in expectedUpdatedAt kontrolünde kullanılır.",
|
|
278
|
+
inputSchema: {
|
|
279
|
+
project: projectParam,
|
|
280
|
+
environment: environmentParam,
|
|
281
|
+
applicationId: guid,
|
|
282
|
+
pageId: guid,
|
|
283
|
+
mode: modeParam,
|
|
284
|
+
componentId: z.string().optional().describe("subtree kipinde kök alınacak komponentin elementId'si")
|
|
285
|
+
},
|
|
286
|
+
annotations: readOnly
|
|
287
|
+
}, handle(config, async ({ project, environment, applicationId, pageId, mode, componentId }) => {
|
|
288
|
+
const page = await fetchPage(platform, project, environment, applicationId, pageId);
|
|
289
|
+
const updatedAt = page.audit?.updatedAt ?? page.audit?.createdAt;
|
|
290
|
+
if (mode === 'full')
|
|
291
|
+
return { updatedAt, schema: page.schema ?? [] };
|
|
292
|
+
if (mode === 'subtree') {
|
|
293
|
+
if (!componentId)
|
|
294
|
+
throw new PlatformApiError("mode='subtree' için componentId zorunludur.");
|
|
295
|
+
const subtree = findNode(page.schema, componentId);
|
|
296
|
+
if (!subtree)
|
|
297
|
+
throw new PlatformApiError(`Sayfada '${componentId}' id'li bir komponent yok; page_schema_get'i summary kipinde çağırıp hedefi doğrulayın.`);
|
|
298
|
+
return { updatedAt, schema: subtree };
|
|
299
|
+
}
|
|
300
|
+
return { updatedAt, schema: (page.schema ?? []).map(summarize) };
|
|
301
|
+
}));
|
|
302
|
+
server.registerTool('page_state', {
|
|
303
|
+
title: 'Sayfa durumunu getir',
|
|
304
|
+
description: 'Sayfanın state ağacı: page/app değişkenleri, sayfadaki komponentler, layout slotları ve microservice imzaları. Binding ifadelerini yalnız buradaki isimlere bağlayın — isim uydurmayın.',
|
|
305
|
+
inputSchema: { project: projectParam, environment: environmentParam, applicationId: guid, pageId: guid },
|
|
306
|
+
annotations: readOnly
|
|
307
|
+
}, handle(config, ({ project, environment, applicationId, pageId }) => platform.tenant('GET', project, `/pages/${applicationId}/${pageId}/ai-context`, { environment })));
|
|
308
|
+
server.registerTool('page_schema_validate', {
|
|
309
|
+
title: 'Sayfa şemasını doğrula (kuru çalıştırma)',
|
|
310
|
+
description: "Micro-op'ları kaydetmeden şemaya uygular ve doğrulama hatalarını (suggestions ile) döndürür; page_schema_patch öncesi planı test etmek için. Gövdeye page_schema_patch ile AYNI normalizasyonu uygular (Prop sarmalı, validation biçimi) — kuru koşu ile yazma aynı gövdeyi görür.",
|
|
311
|
+
inputSchema: { project: projectParam, environment: environmentParam, applicationId: guid, pageId: guid, ops: opsParam.optional() },
|
|
312
|
+
annotations: readOnly
|
|
313
|
+
}, handle(config, async ({ project, environment, applicationId, pageId, ops }) => {
|
|
314
|
+
const opRecords = (ops ?? []).map(op => op);
|
|
315
|
+
const normalized = normalizeSchemaOps(opRecords);
|
|
316
|
+
const validationIssues = schemaOpValidationErrors(opRecords);
|
|
317
|
+
const response = ((await platform.tenant('POST', project, `/pages/${applicationId}/${pageId}/schema/validate`, {
|
|
318
|
+
environment,
|
|
319
|
+
body: { ops: opRecords }
|
|
320
|
+
})) ?? {});
|
|
321
|
+
if (validationIssues.length > 0)
|
|
322
|
+
response.validationWarnings = validationIssues.map(issue => `PAGE-016 — ${issue}`);
|
|
323
|
+
return withNormalization(response, [
|
|
324
|
+
...componentDefaultsNote(normalized.components),
|
|
325
|
+
...validationNote(normalized.validations),
|
|
326
|
+
...replacePropNote(normalized.props)
|
|
327
|
+
]);
|
|
328
|
+
}));
|
|
329
|
+
server.registerTool('page_schema_patch', {
|
|
330
|
+
title: 'Sayfa şemasına patch uygula',
|
|
331
|
+
description: "Micro-op'ları doğrulama duvarından geçirip kaydeder; açık studio canvas'ları SignalR ile canlı güncellenir. expectedUpdatedAt verilirse sayfa o okumadan beri değişmişse istek reddedilir (stale plan koruması). Yanıttaki appliedPatch, her op'un şemada tam olarak hangi JSON Pointer'a dokunduğunu söyler.",
|
|
332
|
+
inputSchema: {
|
|
333
|
+
project: projectParam,
|
|
334
|
+
environment: environmentParam,
|
|
335
|
+
applicationId: guid,
|
|
336
|
+
pageId: guid,
|
|
337
|
+
ops: opsParam,
|
|
338
|
+
expectedUpdatedAt: z.string().optional().describe("page_schema_get çıktısındaki updatedAt; eşleşmezse patch uygulanmaz")
|
|
339
|
+
},
|
|
340
|
+
annotations: updates
|
|
341
|
+
}, handle(config, async ({ project, environment, applicationId, pageId, ops, expectedUpdatedAt }) => {
|
|
342
|
+
const allOps = ops.map(op => op);
|
|
343
|
+
const removeEventOps = allOps.filter(entry => entry.op === 'remove_event');
|
|
344
|
+
const opRecords = allOps.filter(entry => entry.op !== 'remove_event');
|
|
345
|
+
const containerOps = opRecords
|
|
346
|
+
.filter(entry => (entry.op === 'remove_component' || entry.op === 'move_component') && entry.target === PAGE_CONTAINER_ELEMENT_ID);
|
|
347
|
+
if (containerOps.length > 0) {
|
|
348
|
+
throw new PlatformApiError(`'${PAGE_CONTAINER_ELEMENT_ID}' sayfanın kök kapsayıcısıdır; silinmez ve taşınmaz (PAGE-006) — silinirse sayfa iskeleti bozulur ve Studio komponentleri kapsayıcı dışında bırakır. Komponent kaldırmak için remove_component'i kapsayıcının İÇİNDEKİ elementId'lerle kullanın (page_schema_get ile listeleyin).`, 400);
|
|
349
|
+
}
|
|
350
|
+
let cachedPage;
|
|
351
|
+
const readPage = async () => {
|
|
352
|
+
cachedPage ??= await fetchPage(platform, project, environment, applicationId, pageId);
|
|
353
|
+
return cachedPage;
|
|
354
|
+
};
|
|
355
|
+
if (expectedUpdatedAt) {
|
|
356
|
+
const current = await readPage();
|
|
357
|
+
const updatedAt = current.audit?.updatedAt ?? current.audit?.createdAt;
|
|
358
|
+
if (updatedAt !== expectedUpdatedAt) {
|
|
359
|
+
throw new PlatformApiError(`Sayfa siz okuduktan sonra değişmiş (updatedAt: ${updatedAt}). page_schema_get ile yeniden okuyup planı tazeleyin.`, 409);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
const normalized = normalizeSchemaOps(opRecords);
|
|
363
|
+
const validationIssues = schemaOpValidationErrors(opRecords);
|
|
364
|
+
if (validationIssues.length > 0) {
|
|
365
|
+
throw new PlatformApiError(`PAGE-016 — validation gövdesi eksik (istek gönderilmedi): ${validationIssues.join(' · ')}. `
|
|
366
|
+
+ "Çalışan biçim 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 }.", 400);
|
|
367
|
+
}
|
|
368
|
+
const wantsRootAdd = opRecords.some(entry => entry.op === 'add_component' && (entry.parent === undefined || entry.parent === null || entry.parent === 'root'));
|
|
369
|
+
let retargeted = [];
|
|
370
|
+
if (wantsRootAdd) {
|
|
371
|
+
const page = await readPage();
|
|
372
|
+
if ((page.schema ?? []).some(node => node.elementId === PAGE_CONTAINER_ELEMENT_ID)) {
|
|
373
|
+
retargeted = retargetRootAdds(opRecords, PAGE_CONTAINER_ELEMENT_ID);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
if (opRecords.some(entry => entry.op === 'set_event')) {
|
|
377
|
+
const typesByElementId = new Map();
|
|
378
|
+
try {
|
|
379
|
+
collectTypes((await readPage()).schema, typesByElementId);
|
|
380
|
+
}
|
|
381
|
+
catch {
|
|
382
|
+
typesByElementId.clear();
|
|
383
|
+
}
|
|
384
|
+
collectTypes(opRecords.map(entry => entry.component).filter(Boolean), typesByElementId);
|
|
385
|
+
const eventsBySlug = await fetchComponentEventNames(platform, project, environment, applicationId).catch(() => new Map());
|
|
386
|
+
const unknown = unknownEventOps(opRecords, typesByElementId, eventsBySlug);
|
|
387
|
+
if (unknown.length > 0)
|
|
388
|
+
throw new PlatformApiError(`PAGE-008 — geçersiz olay adı (istek gönderilmedi): ${unknown.join(' · ')}`, 400);
|
|
389
|
+
}
|
|
390
|
+
const added = opRecords
|
|
391
|
+
.filter(entry => entry.op === 'add_component')
|
|
392
|
+
.map(entry => entry.component);
|
|
393
|
+
const badWidgets = invalidWidgetTypeNodes(added, 'PAGE-012');
|
|
394
|
+
if (badWidgets.length > 0)
|
|
395
|
+
throw new PlatformApiError(`Geçersiz widget düğümü (istek gönderilmedi): ${badWidgets.join(' · ')}`, 400);
|
|
396
|
+
const badNames = invalidComponentNames(added);
|
|
397
|
+
if (badNames.length > 0) {
|
|
398
|
+
throw new PlatformApiError(`PAGE-009 — komponent adı JS tanımlayıcısı değil (istek gönderilmedi): ${badNames.join(' · ')}. `
|
|
399
|
+
+ 'Runtime her komponent için `const <ad> = Components["<ad>"]` üretir; boşluklu ya da geçersiz bir ad '
|
|
400
|
+
+ 'SAYFADAKİ TÜM ifadeleri "Missing initializer in const declaration" ile öldürür (layout adları her sayfaya sızar). '
|
|
401
|
+
+ 'name alanını elementId ile aynı yaz; okunabilir Türkçe başlık için props.title / props.text / props.label kullan.', 400);
|
|
402
|
+
}
|
|
403
|
+
const catalog = added.length > 0
|
|
404
|
+
? await applyCatalogLayoutRules(platform, project, environment, applicationId, added)
|
|
405
|
+
: { clamped: [], heights: [] };
|
|
406
|
+
const formWarnings = formBindingWarnings(opRecords);
|
|
407
|
+
let flowWarnings = [];
|
|
408
|
+
let layoutWarnings = [];
|
|
409
|
+
if (opRecords.some(entry => entry.op === 'remove_component')) {
|
|
410
|
+
try {
|
|
411
|
+
const page = await readPage();
|
|
412
|
+
flowWarnings = orphanedFlowWarnings(page.schema, page.events, opRecords);
|
|
413
|
+
}
|
|
414
|
+
catch {
|
|
415
|
+
flowWarnings = [];
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
let nestedAfterOps = [];
|
|
419
|
+
const simulatable = opRecords.some(entry => ['add_component', 'move_component'].includes(String(entry.op))
|
|
420
|
+
|| (entry.op === 'replace_prop' && FLEX_LAYOUT_PROP_NAMES.has(String(entry.prop ?? ''))));
|
|
421
|
+
if (simulatable) {
|
|
422
|
+
try {
|
|
423
|
+
const page = await readPage();
|
|
424
|
+
const simulated = JSON.parse(JSON.stringify(page.schema ?? []));
|
|
425
|
+
const touched = simulateLayoutOps(simulated, opRecords);
|
|
426
|
+
// PAGE-013 op'lar UYGULANDIKTAN SONRAKİ ağaca bakar: tek başına eklenen düğüm masum
|
|
427
|
+
// görünür, iç içeliği ancak hedef parent zinciri belli olunca anlarsın.
|
|
428
|
+
nestedAfterOps = nestedRepeaterNodes(simulated);
|
|
429
|
+
layoutWarnings = [
|
|
430
|
+
...pageContainerPositionWarning(page.schema),
|
|
431
|
+
...verticalGapWarnings(simulated, 'PAGE-010 (uyarı)', touched),
|
|
432
|
+
...stringStyleWarning(stringStyleNodes(simulated)),
|
|
433
|
+
...inertFlexPropWarning(inertFlexPropNodes(simulated))
|
|
434
|
+
];
|
|
435
|
+
}
|
|
436
|
+
catch {
|
|
437
|
+
layoutWarnings = [];
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
if (nestedAfterOps.length > 0)
|
|
441
|
+
throw new PlatformApiError(nestedRepeaterMessage(nestedAfterOps), 400);
|
|
442
|
+
const removedEvents = removeEventOps.length > 0
|
|
443
|
+
? await applyEventRemovals(platform, project, environment, applicationId, pageId, await readPage(), removeEventOps)
|
|
444
|
+
: [];
|
|
445
|
+
if (removeEventOps.length > 0)
|
|
446
|
+
cachedPage = undefined;
|
|
447
|
+
const response = opRecords.length > 0
|
|
448
|
+
? ((await platform.tenant('PATCH', project, `/pages/${applicationId}/${pageId}/schema`, { environment, body: { ops: opRecords } })) ?? {})
|
|
449
|
+
: { valid: true };
|
|
450
|
+
if (removedEvents.length > 0)
|
|
451
|
+
response.removedEvents = removedEvents;
|
|
452
|
+
if (formWarnings.length > 0)
|
|
453
|
+
response.formWarnings = formWarnings;
|
|
454
|
+
if (flowWarnings.length > 0)
|
|
455
|
+
response.flowWarnings = flowWarnings;
|
|
456
|
+
if (layoutWarnings.length > 0)
|
|
457
|
+
response.layoutWarnings = layoutWarnings;
|
|
458
|
+
return withNormalization(response, [
|
|
459
|
+
...rootAddsNote(retargeted, PAGE_CONTAINER_ELEMENT_ID),
|
|
460
|
+
...componentDefaultsNote(normalized.components),
|
|
461
|
+
...validationNote(normalized.validations),
|
|
462
|
+
...blockSizeNote(catalog.clamped),
|
|
463
|
+
...heightTypeNote(catalog.heights),
|
|
464
|
+
...replacePropNote(normalized.props)
|
|
465
|
+
]);
|
|
466
|
+
}));
|
|
467
|
+
server.registerTool('page_create', {
|
|
468
|
+
title: 'Sayfa oluştur',
|
|
469
|
+
description: "Yeni sayfa açar. Şemayı burada göndermek yerine boş bırakıp page_schema_patch micro-op'larıyla kurmak daha doğrudur (doğrulama duvarı + canlı canvas orada). roleIds için role_list, layoutId için layout_list kullanın.",
|
|
470
|
+
inputSchema: {
|
|
471
|
+
project: projectParam,
|
|
472
|
+
environment: environmentParam,
|
|
473
|
+
applicationId: guid,
|
|
474
|
+
name: z.string().min(1),
|
|
475
|
+
slug: z.string().min(1).describe("URL yolu, örn. 'orders' ya da 'orders/detail'"),
|
|
476
|
+
browserTitle: z.string().optional(),
|
|
477
|
+
layoutId: guid.optional(),
|
|
478
|
+
roleIds: z.array(guid).optional().describe('Sayfayı görebilecek roller; boş = herkese açık'),
|
|
479
|
+
middlewareIds: z.array(guid).optional(),
|
|
480
|
+
events: eventsParam.optional(),
|
|
481
|
+
schema: schemaParam.optional()
|
|
482
|
+
},
|
|
483
|
+
annotations: writes
|
|
484
|
+
}, handle(config, async ({ project, environment, applicationId, name, slug, browserTitle, layoutId, roleIds, middlewareIds, events, schema }) => {
|
|
485
|
+
const badEvents = invalidScreenEventNames(events, 'PAGE-011');
|
|
486
|
+
if (badEvents.length > 0)
|
|
487
|
+
throw new PlatformApiError(`Geçersiz sayfa olayı (istek gönderilmedi): ${badEvents.join(' · ')}`, 400);
|
|
488
|
+
const container = ensurePageContainer(schema ?? []);
|
|
489
|
+
const badWidgets = invalidWidgetTypeNodes(container.schema, 'PAGE-012');
|
|
490
|
+
if (badWidgets.length > 0)
|
|
491
|
+
throw new PlatformApiError(`Geçersiz widget düğümü (istek gönderilmedi): ${badWidgets.join(' · ')}`, 400);
|
|
492
|
+
const nested = nestedRepeaterNodes(container.schema);
|
|
493
|
+
if (nested.length > 0)
|
|
494
|
+
throw new PlatformApiError(nestedRepeaterMessage(nested), 400);
|
|
495
|
+
const stringStyles = stringStyleNodes(container.schema);
|
|
496
|
+
const fixed = normalizeComponentDefaults(container.schema);
|
|
497
|
+
const reset = resetPageContainerPosition(container.schema);
|
|
498
|
+
const catalog = (schema ?? []).length > 0
|
|
499
|
+
? await applyCatalogLayoutRules(platform, project, environment, applicationId, container.schema)
|
|
500
|
+
: { clamped: [], heights: [] };
|
|
501
|
+
const moved = reorderSchemaBlocks(container.schema);
|
|
502
|
+
const gaps = (schema ?? []).length > 0 ? verticalGapWarnings(container.schema, 'PAGE-010 (uyarı)') : [];
|
|
503
|
+
const response = await platform.tenant('POST', project, `/pages/${applicationId}`, {
|
|
504
|
+
environment,
|
|
505
|
+
body: {
|
|
506
|
+
name,
|
|
507
|
+
slug,
|
|
508
|
+
browserTitle: browserTitle ?? '',
|
|
509
|
+
layoutId: layoutId ?? null,
|
|
510
|
+
roleIds: roleIds ?? [],
|
|
511
|
+
middlewareIds: middlewareIds ?? [],
|
|
512
|
+
events: events ?? [],
|
|
513
|
+
schema: container.schema
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
const normalized = withNormalization(response, [...container.fixes, ...reset, ...componentDefaultsNote(fixed), ...blockSizeNote(catalog.clamped), ...heightTypeNote(catalog.heights), ...reorderNote(moved)]);
|
|
517
|
+
const createWarnings = [...gaps, ...stringStyleWarning(stringStyles), ...inertFlexPropWarning(inertFlexPropNodes(container.schema))];
|
|
518
|
+
if (createWarnings.length === 0 || !normalized || typeof normalized !== 'object')
|
|
519
|
+
return normalized;
|
|
520
|
+
return { ...normalized, layoutWarnings: createWarnings };
|
|
521
|
+
}));
|
|
522
|
+
server.registerTool('page_update', {
|
|
523
|
+
title: 'Sayfa üstbilgisini güncelle',
|
|
524
|
+
description: "Sayfanın adını/slug'ını/layout'unu/rollerini değiştirir. Platformun PUT'u REPLACE'tir; gateway sayfayı önce okur, yalnız verdiğiniz alanları değiştirip tam gövdeyi geri yazar — bu yüzden şema ve olaylar kaybolmaz. ŞEMA değişikliği için bu tool'u değil page_schema_patch'i kullanın (doğrulama duvarı + canlı canvas orada).",
|
|
525
|
+
inputSchema: {
|
|
526
|
+
project: projectParam,
|
|
527
|
+
environment: environmentParam,
|
|
528
|
+
applicationId: guid,
|
|
529
|
+
pageId: guid,
|
|
530
|
+
name: z.string().min(1).optional(),
|
|
531
|
+
slug: z.string().min(1).optional(),
|
|
532
|
+
browserTitle: z.string().optional(),
|
|
533
|
+
layoutId: guid.nullable().optional().describe('null: layout bağını kaldırır'),
|
|
534
|
+
roleIds: z.array(guid).optional(),
|
|
535
|
+
middlewareIds: z.array(guid).optional(),
|
|
536
|
+
events: eventsParam.optional(),
|
|
537
|
+
schema: schemaParam.optional().describe('Nadiren gerekir; verilmezse mevcut şema korunur')
|
|
538
|
+
},
|
|
539
|
+
annotations: updates
|
|
540
|
+
}, handle(config, async ({ project, environment, applicationId, pageId, ...changes }) => {
|
|
541
|
+
const overrides = Object.fromEntries(Object.entries(changes).filter(([, value]) => value !== undefined));
|
|
542
|
+
const badEvents = invalidScreenEventNames(overrides.events, 'PAGE-011');
|
|
543
|
+
if (badEvents.length > 0)
|
|
544
|
+
throw new PlatformApiError(`Geçersiz sayfa olayı (istek gönderilmedi): ${badEvents.join(' · ')}`, 400);
|
|
545
|
+
const badWidgets = invalidWidgetTypeNodes(overrides.schema, 'PAGE-012');
|
|
546
|
+
if (badWidgets.length > 0)
|
|
547
|
+
throw new PlatformApiError(`Geçersiz widget düğümü (istek gönderilmedi): ${badWidgets.join(' · ')}`, 400);
|
|
548
|
+
const nestedUpdate = nestedRepeaterNodes(overrides.schema);
|
|
549
|
+
if (nestedUpdate.length > 0)
|
|
550
|
+
throw new PlatformApiError(nestedRepeaterMessage(nestedUpdate), 400);
|
|
551
|
+
const current = await fetchHost(platform, 'page', project, environment, applicationId, pageId);
|
|
552
|
+
const badSchemaNames = invalidComponentNames(overrides.schema);
|
|
553
|
+
if (badSchemaNames.length > 0) {
|
|
554
|
+
throw new PlatformApiError(`PAGE-009 — komponent adı JS tanımlayıcısı değil (istek gönderilmedi): ${badSchemaNames.join(' · ')}. `
|
|
555
|
+
+ 'Runtime her komponent için `const <ad> = Components["<ad>"]` üretir; geçersiz bir ad sayfadaki TÜM ifadeleri öldürür. '
|
|
556
|
+
+ 'name = elementId yazın.', 400);
|
|
557
|
+
}
|
|
558
|
+
const fixed = normalizeComponentDefaults(overrides.schema);
|
|
559
|
+
const reset = overrides.schema === undefined ? [] : resetPageContainerPosition(overrides.schema);
|
|
560
|
+
const catalog = overrides.schema === undefined
|
|
561
|
+
? { clamped: [], heights: [] }
|
|
562
|
+
: await applyCatalogLayoutRules(platform, project, environment, applicationId, overrides.schema);
|
|
563
|
+
const moved = overrides.schema === undefined ? [] : reorderSchemaBlocks(overrides.schema);
|
|
564
|
+
const gaps = overrides.schema === undefined
|
|
565
|
+
? []
|
|
566
|
+
: [...verticalGapWarnings(overrides.schema, 'PAGE-010 (uyarı)'), ...inertFlexPropWarning(inertFlexPropNodes(overrides.schema))];
|
|
567
|
+
const body = pageUpdateBody(current, overrides);
|
|
568
|
+
await platform.tenant('PUT', project, `/pages/${applicationId}/${pageId}`, { environment, body });
|
|
569
|
+
const summary = {
|
|
570
|
+
id: pageId,
|
|
571
|
+
changed: Object.keys(overrides),
|
|
572
|
+
preserved: { schemaNodes: body.schema.length, events: body.events.length }
|
|
573
|
+
};
|
|
574
|
+
if (gaps.length > 0)
|
|
575
|
+
summary.layoutWarnings = gaps;
|
|
576
|
+
return withNormalization(summary, [...reset, ...componentDefaultsNote(fixed), ...blockSizeNote(catalog.clamped), ...heightTypeNote(catalog.heights), ...reorderNote(moved)]);
|
|
577
|
+
}));
|
|
578
|
+
server.registerTool('page_clone', {
|
|
579
|
+
title: 'Sayfa klonla',
|
|
580
|
+
description: "Sayfanın kopyasını oluşturur (şema, olaylar ve flow'larıyla). Klon ana sayfa olarak işaretlenmez.",
|
|
581
|
+
inputSchema: { project: projectParam, environment: environmentParam, applicationId: guid, pageId: guid },
|
|
582
|
+
annotations: writes
|
|
583
|
+
}, handle(config, ({ project, environment, applicationId, pageId }) => platform.tenant('POST', project, `/pages/${applicationId}/clone/${pageId}`, { environment })));
|
|
584
|
+
server.registerTool('page_delete', {
|
|
585
|
+
title: 'Sayfa sil',
|
|
586
|
+
description: 'Sayfayı siler. Ana sayfa (isHomepage) silinemez; önce başka bir sayfayı ana sayfa yapın. Silmeden önce dependency_graph module:\'page\' ile bağımlılıkları kontrol edin.',
|
|
587
|
+
inputSchema: { project: projectParam, environment: environmentParam, applicationId: guid, pageId: guid },
|
|
588
|
+
annotations: destructive
|
|
589
|
+
}, handle(config, async ({ project, environment, applicationId, pageId }) => {
|
|
590
|
+
const page = unwrap(await platform.tenant('GET', project, `/pages/${applicationId}/${pageId}`, { environment }));
|
|
591
|
+
if (page.isHomepage === true) {
|
|
592
|
+
throw new PlatformApiError(`'${page.name}' ana sayfadır ve silinemez (CantDeleteHomepage); önce başka bir sayfa ana sayfa yapılmalı.`);
|
|
593
|
+
}
|
|
594
|
+
await platform.tenant('DELETE', project, `/pages/${applicationId}/${pageId}`, { environment });
|
|
595
|
+
return { deleted: pageId, name: page.name };
|
|
596
|
+
}));
|
|
597
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { listKnowledgeFiles, readKnowledgeFile } from '../resources/knowledge.js';
|
|
2
|
+
import { logCapabilityGap } from '../observability/trace.js';
|
|
3
|
+
import { handle } from '../platform/responses.js';
|
|
4
|
+
import { readOnly } from './common.js';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
export function registerPlatformTools(server, platform) {
|
|
7
|
+
const config = platform.config;
|
|
8
|
+
server.registerTool('platform_whoami', {
|
|
9
|
+
title: 'Oturum bilgisi',
|
|
10
|
+
description: 'Gateway\'in platformda kimin adına çalıştığını döner; auth kurulumunu doğrulamak için ilk çağrılacak tool.',
|
|
11
|
+
inputSchema: {},
|
|
12
|
+
annotations: readOnly
|
|
13
|
+
}, handle(config, () => platform.org('GET', '/connect/userinfo')));
|
|
14
|
+
server.registerTool('platform_guide', {
|
|
15
|
+
title: 'Platform rehberi oku',
|
|
16
|
+
description: "Modülün iş kuralları rehberini döner (workflow, microservice, entity, page, component, html, middleware, view, expressions, getting-started, modify-existing). Bir modülde İLK yazma işleminden önce ilgili rehberi okuyun; sayfa/layout/flow işleri için page, komponent modeli/olay sözleşmeleri için component, hazır komponentler yetmediğinde HTML kaçış kapısı (Mustache, fireEvent, script kuralları) için html, auth/oturum/yetki için middleware, tam metin arama indeksi için view, ifade yazarken kullanılabilir fonksiyon/bağlam sözlüğü (system.*, file.*, Page.*, state.*, Steps…) için expressions rehberi. Örnek gövdeler için ngapps://examples kaynaklarına bakın.",
|
|
17
|
+
inputSchema: {
|
|
18
|
+
guide: z.string().describe('Rehber adı; bilinmeyen ad verilirse mevcut rehberler listelenir')
|
|
19
|
+
},
|
|
20
|
+
annotations: readOnly
|
|
21
|
+
}, handle(config, async ({ guide }) => {
|
|
22
|
+
const available = listKnowledgeFiles('guides').map(file => file.replace(/\.md$/, ''));
|
|
23
|
+
if (!available.includes(guide)) {
|
|
24
|
+
return { error: `'${guide}' diye bir rehber yok.`, available };
|
|
25
|
+
}
|
|
26
|
+
return { guide, content: readKnowledgeFile('guides', `${guide}.md`), examples: listKnowledgeFiles('examples').map(file => `ngapps://examples/${file.replace(/\.json$/, '')}`) };
|
|
27
|
+
}));
|
|
28
|
+
server.registerTool('platform_gap_report', {
|
|
29
|
+
title: 'Kabiliyet boşluğu bildir',
|
|
30
|
+
description: "Yapmak İSTEDİĞİNİZ ama platform desteklemediği için yapamadığınız her noktada BUNU ÇAĞIRIN — tasarımı sessizce küçültmeyin. Bu çağrı platforma gitmez, yalnızca günlüğe 'KABİLİYET BOŞLUĞU' bloğu yazar; hata günlüğünde görünmeyen bu kayıtlar ürün yol haritasının girdisidir. Önce rehbere bakıp (platform_guide, özellikle hazır komponent yetmiyorsa 'html' kaçış kapısı) gerçekten desteklenmediğinden emin olun: desteklenen bir şeyi 'yok' diye bildirmek yanlış yol haritası üretir.",
|
|
31
|
+
inputSchema: {
|
|
32
|
+
goal: z.string().min(1).describe('Ne yapmak istediniz — kullanıcıya görünen sonuç olarak yazın ("randevu listesini hasta adına göre süzmek")'),
|
|
33
|
+
blocker: z.string().min(1).describe('Neden yapamadınız — hangi kabiliyetin eksik olduğu ("uçta X yok", "komponentte Y prop\'u yok")'),
|
|
34
|
+
severity: z.enum(['blocker', 'workaround', 'cosmetic']).describe('blocker: özellik hiç yapılamadı · workaround: daha kötü bir yolla yapıldı · cosmetic: yalnız görsel/ergonomik kayıp'),
|
|
35
|
+
module: z.string().optional().describe('İlgili modül: entity | microservice | workflow | page | component | middleware | view | flow'),
|
|
36
|
+
attempted: z.array(z.string()).optional().describe('Denenen tool/yaklaşımlar — boşluğun gerçekten araştırıldığını gösterir'),
|
|
37
|
+
workaround: z.string().optional().describe('Bunun yerine ne yaptınız; hiçbir şey yapılamadıysa boş bırakın')
|
|
38
|
+
},
|
|
39
|
+
annotations: readOnly
|
|
40
|
+
}, handle(config, async ({ goal, blocker, severity, module, attempted, workaround }) => {
|
|
41
|
+
logCapabilityGap({ goal, blocker, severity, module, attempted, workaround });
|
|
42
|
+
return { recorded: true, severity, module: module ?? null, note: 'Günlüğe yazıldı. Kullanıcıya da tek cümleyle söyleyin: neyi yapamadınız ve yerine ne yaptınız.' };
|
|
43
|
+
}));
|
|
44
|
+
}
|