@appaflytech/zeyrek-ai-mcp 0.0.48 → 0.0.50

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.
Files changed (70) hide show
  1. package/.env.example +5 -0
  2. package/dist/config.js +2 -0
  3. package/dist/knowledge/access.js +4 -3
  4. package/dist/knowledge/assistantRun.js +165 -0
  5. package/dist/knowledge/componentCatalog.js +31 -0
  6. package/dist/knowledge/componentEvents.js +4 -1
  7. package/dist/knowledge/mobileComponentCatalog.js +21 -3
  8. package/dist/knowledge/mobileComponentEvents.js +9 -0
  9. package/dist/knowledge/mobileStore.js +14 -0
  10. package/dist/knowledge/motionResolve.js +1 -1
  11. package/dist/knowledge/platform/lint.js +36 -3
  12. package/dist/knowledge/platform/mobileRules.js +6 -0
  13. package/dist/knowledge/styleResolve.js +7 -0
  14. package/dist/platform/capture/actions.js +67 -0
  15. package/dist/platform/capture/capture.js +26 -21
  16. package/dist/platform/errors.js +9 -0
  17. package/dist/platform/http.js +7 -2
  18. package/dist/platform/storeArt/compose.js +114 -0
  19. package/dist/platform/storeArt/deck.js +133 -0
  20. package/dist/platform/storeArt/gallery.js +331 -0
  21. package/dist/platform/storeArt/layouts.js +121 -0
  22. package/dist/platform/storeArt/styles.js +54 -0
  23. package/dist/platform/tokenVault.js +15 -0
  24. package/dist/resources/examples/mobile-reorder-persist.json +491 -0
  25. package/dist/resources/guides/assistant.md +194 -0
  26. package/dist/resources/guides/deployment.md +19 -1
  27. package/dist/resources/guides/microservice.md +7 -0
  28. package/dist/resources/guides/mobile-publish.md +13 -0
  29. package/dist/resources/guides/mobile.md +4 -0
  30. package/dist/resources/guides/page.md +62 -0
  31. package/dist/resources/prompts/mobile-publish-prepare.md +3 -1
  32. package/dist/resources/schema-fingerprint.json +4 -4
  33. package/dist/resources/schemas/AccessMatrixDto.json +2 -1
  34. package/dist/resources/schemas/AccessPermissionDto.json +2 -1
  35. package/dist/resources/schemas/AccessResourceDto.json +2 -1
  36. package/dist/resources/schemas/AssistantCapabilitiesDto.json +256 -0
  37. package/dist/resources/schemas/AssistantPublishStateDto.json +57 -0
  38. package/dist/resources/schemas/AssistantScenarioDto.json +91 -0
  39. package/dist/resources/schemas/AssistantScenarioRunDto.json +62 -0
  40. package/dist/resources/schemas/AssistantSettingsDto.json +52 -0
  41. package/dist/resources/schemas/AssistantSkillDto.json +78 -0
  42. package/dist/resources/schemas/AssistantSummaryDto.json +100 -0
  43. package/dist/resources/schemas/CreateAssistantScenarioCommand.json +81 -0
  44. package/dist/resources/schemas/CreateAssistantSkillCommand.json +83 -0
  45. package/dist/resources/schemas/CreateMicroserviceCommand.json +16 -0
  46. package/dist/resources/schemas/DescribeAssistantCapabilityCommand.json +145 -0
  47. package/dist/resources/schemas/GlobalFunctionUpdateResultDto.json +5 -1
  48. package/dist/resources/schemas/MicroserviceDto.json +16 -0
  49. package/dist/resources/schemas/PublishAssistantCommand.json +15 -0
  50. package/dist/resources/schemas/RunAssistantScenariosCommand.json +39 -0
  51. package/dist/resources/schemas/SimulateAssistantCommand.json +106 -0
  52. package/dist/resources/schemas/UpdateAssistantScenarioCommand.json +79 -0
  53. package/dist/resources/schemas/UpdateAssistantSettingsCommand.json +52 -0
  54. package/dist/resources/schemas/UpdateAssistantSkillCommand.json +81 -0
  55. package/dist/resources/schemas/UpdateMicroserviceCommand.json +16 -0
  56. package/dist/resources/store-art/editor.css +389 -0
  57. package/dist/resources/store-art/editor.html +123 -0
  58. package/dist/resources/store-art/editor.js +990 -0
  59. package/dist/server.js +2 -0
  60. package/dist/tools/applications.js +4 -4
  61. package/dist/tools/assistant.js +382 -0
  62. package/dist/tools/deployments.js +9 -3
  63. package/dist/tools/microservices.js +1 -1
  64. package/dist/tools/mobile.js +2 -2
  65. package/dist/tools/mobileStore.js +56 -19
  66. package/dist/tools/pages.js +21 -5
  67. package/dist/tools/projects.js +2 -2
  68. package/dist/tools/templates.js +1 -1
  69. package/dist/tools/themes.js +2 -2
  70. package/package.json +2 -2
@@ -188,6 +188,31 @@ async function resolveClip(page, clip) {
188
188
  }
189
189
  return { x: Math.round(raw.x), y: Math.round(raw.y), width: Math.round(raw.width), height: Math.round(raw.height) };
190
190
  }
191
+ /**
192
+ * Gateway'in varsayılan tarayıcısı: kalıcı profilli Chromium. `mobile_asset_preview` de bunu kullanır
193
+ * (ADR 0004); aynı süreçte ikinci açılış profil dizini dolu olduğu için sıradaki yuvaya geçer.
194
+ */
195
+ export async function launchScreenshotBrowser(config) {
196
+ let chromium;
197
+ try {
198
+ chromium = (await import('playwright-core')).chromium;
199
+ }
200
+ catch {
201
+ throw new PlatformApiError('playwright-core paketi yüklü değil — zeyrek-mcp dizininde `npm install playwright-core` çalıştırın, ardından tarayıcı için `npx playwright-core install chromium`.');
202
+ }
203
+ logStep('tarayıcı →', `chromium başlatılıyor (ilk çağrı — sonraki çağrılar önbellekteki tarayıcıyı kullanır) · profil ${screenshotProfileRoot()}`);
204
+ const open = persistentContextOpener(chromium, config.screenshotBrowserPath ? { executablePath: config.screenshotBrowserPath } : {});
205
+ const browser = new PersistentScreenshotBrowser(open);
206
+ try {
207
+ // İlk bağlam burada açılır ki başlatma hatası eskisi gibi `launch()`'tan dönsün.
208
+ await (await browser.newContext({ viewport: { width: 1280, height: 720 }, deviceScaleFactor: 1 })).close();
209
+ }
210
+ catch (firstError) {
211
+ const message = firstError instanceof Error ? firstError.message : String(firstError);
212
+ throw new PlatformApiError(`Chromium başlatılamadı: ${message}. \`npx playwright-core install chromium\` ile indirin; alternatif olarak sistemdeki tarayıcının yolunu SCREENSHOT_BROWSER_PATH ile verin (alpine imajında \`apk add chromium\` + /usr/bin/chromium-browser).`);
213
+ }
214
+ return browser;
215
+ }
191
216
  /**
192
217
  * Playwright bağlamı ve tarayıcısı bu sınıfta PAYLAŞILIR. İki çekim aynı anda koştuğunda
193
218
  * 2026-09-06 koşusunda ikisi de `Target page, context or browser has been closed` ile düştü
@@ -272,27 +297,7 @@ export class StudioScreenshotter {
272
297
  }
273
298
  }
274
299
  async launch() {
275
- if (this.launcher)
276
- return this.launcher();
277
- let chromium;
278
- try {
279
- chromium = (await import('playwright-core')).chromium;
280
- }
281
- catch {
282
- throw new PlatformApiError('playwright-core paketi yüklü değil — zeyrek-mcp dizininde `npm install playwright-core` çalıştırın, ardından tarayıcı için `npx playwright-core install chromium`.');
283
- }
284
- logStep('tarayıcı →', `chromium başlatılıyor (ilk çağrı — sonraki çağrılar önbellekteki tarayıcıyı kullanır) · profil ${screenshotProfileRoot()}`);
285
- const open = persistentContextOpener(chromium, this.config.screenshotBrowserPath ? { executablePath: this.config.screenshotBrowserPath } : {});
286
- const browser = new PersistentScreenshotBrowser(open);
287
- try {
288
- // İlk bağlam burada açılır ki başlatma hatası eskisi gibi `launch()`'tan dönsün.
289
- await (await browser.newContext({ viewport: { width: 1280, height: 720 }, deviceScaleFactor: 1 })).close();
290
- }
291
- catch (firstError) {
292
- const message = firstError instanceof Error ? firstError.message : String(firstError);
293
- throw new PlatformApiError(`Chromium başlatılamadı: ${message}. \`npx playwright-core install chromium\` ile indirin; alternatif olarak sistemdeki tarayıcının yolunu SCREENSHOT_BROWSER_PATH ile verin (alpine imajında \`apk add chromium\` + /usr/bin/chromium-browser).`);
294
- }
295
- return browser;
300
+ return this.launcher ? this.launcher() : launchScreenshotBrowser(this.config);
296
301
  }
297
302
  /**
298
303
  * Bekleme stratejisi bilinçli olarak `networkidle` DEĞİL: Studio'nun kök layout'u
@@ -472,6 +472,15 @@ export function mapPlatformError(status, payload, context) {
472
472
  const errorBodyDetail = errorBody.length > 0 ? ` — ${errorBody.join(' · ')}` : '';
473
473
  return new PlatformApiError(`Platform hatası ${status} (${location}): ${apiMessage ?? 'beklenmeyen hata'}${code ? ` [${code}]` : ''}${errorBodyDetail}${fallbackDetail}${diagnoseKnownFailure(status, apiMessage ?? '', code, context)}`, status, code);
474
474
  }
475
+ const CONNECTION_RESET_CODES = new Set(['ECONNRESET', 'UND_ERR_SOCKET', 'EPIPE', 'ECONNABORTED']);
476
+ /**
477
+ * Bağlantı yolda koptu mu (yanıt gelmeden): Docker Desktop'ın host port yönlendirmesi yük altında açık bağlantıları ara ara
478
+ * düşürür — konteyner içinden aynı uç kopmaz (f-02, 2026-10-24 ölçümü: içeride 0/1800, host'ta `:3000` 4/1800, `:80` 2/1800).
479
+ */
480
+ export function isConnectionReset(error) {
481
+ const failure = error;
482
+ return CONNECTION_RESET_CODES.has(String(failure?.code)) || CONNECTION_RESET_CODES.has(String(failure?.cause?.code));
483
+ }
475
484
  export function mapNetworkError(error, context) {
476
485
  const cause = error instanceof Error ? error : new Error(String(error));
477
486
  if (cause.name === 'TimeoutError' || cause.name === 'AbortError') {
@@ -1,4 +1,4 @@
1
- import { PlatformApiError, mapNetworkError, mapPlatformError } from './errors.js';
1
+ import { PlatformApiError, isConnectionReset, mapNetworkError, mapPlatformError } from './errors.js';
2
2
  import { logStep, logsDetail, renderJson } from '../observability/trace.js';
3
3
  import { AsyncLocalStorage } from 'node:async_hooks';
4
4
  import { TokenProvider } from './token.js';
@@ -134,7 +134,7 @@ export class Platform {
134
134
  }
135
135
  return `Bearer ${await this.tokens.getToken()}`;
136
136
  }
137
- async send(api, method, path, options, raw = false, isRetry = false) {
137
+ async send(api, method, path, options, raw = false, isRetry = false, networkRetried = false) {
138
138
  const baseUrl = api === 'org' ? this.config.orgApiBaseUrl : this.config.tenantApiBaseUrl;
139
139
  const context = { api, method, path, baseUrl, environment: options.environment, authMode: this.config.authMode };
140
140
  const url = new URL(`${baseUrl}${path}`);
@@ -207,6 +207,11 @@ export class Platform {
207
207
  }
208
208
  catch (error) {
209
209
  logStep('ağ ✕', `${method} ${url.href} · ${Date.now() - startedAt} ms sonra ulaşılamadı`, [error instanceof Error ? `${error.name}: ${error.message}` : String(error)]);
210
+ // Yalnız tekrarı zararsız istek (GET/HEAD) ve yalnız bir kez: yazan istek sunucuya ulaşmış olabilir.
211
+ if ((method === 'GET' || method === 'HEAD') && !networkRetried && !options.noNetworkRetry && isConnectionReset(error)) {
212
+ logStep('ağ ⟳', `${method} ${url.href} · bağlantı koptu, bir kez yeniden deneniyor`);
213
+ return this.send(api, method, path, options, raw, isRetry, true);
214
+ }
210
215
  throw mapNetworkError(error, context);
211
216
  }
212
217
  logStep('yanıt ←', `HTTP ${statusCode} · ${Date.now() - startedAt} ms · ${responseBytes} B`, statusCode >= 400 || logsDetail()
@@ -0,0 +1,114 @@
1
+ import { mkdir, rm, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
4
+ import { PlatformApiError } from '../errors.js';
5
+ import { imageSize } from '../capture/capture.js';
6
+ import { STORE_ART_LAYOUTS, storeCanvas } from './layouts.js';
7
+ import { BACKDROPS, FONT_STACKS, resolveStyle } from './styles.js';
8
+ import { cardFileName } from './deck.js';
9
+ /**
10
+ * Mağaza pazarlama karesi (ADR 0004, 0005): ham ekran görüntüsü yerleşimin kutularına oturtulur, stilin
11
+ * zemini, yazısı ve sahnesiyle mağazanın istediği ölçüde alfasız JPEG çizilir. Çizim gateway'in Chromium'uyla
12
+ * yapılır; kare HTML'i saf fonksiyondur ve yerleşime ya da stile göre dallanmaz — ikisi de kayıttan okunur.
13
+ * Editörün önizlemesi de aynı HTML'dir (`cardHtml`); önizleme ile çıktı ayrışamaz.
14
+ */
15
+ export { storeCanvas } from './layouts.js';
16
+ const jpegQuality = 92;
17
+ const renderTimeoutMs = 30000;
18
+ /**
19
+ * Görseller `decode()` ile beklenir: `domcontentloaded` anında `<img>` henüz çözülmemiş olabilir ve kare
20
+ * boş ekranla çıkardı. Yazı tipi de beklenir; satır kırılımı yüklenen yazı tipine göre değişir.
21
+ */
22
+ const slideReadyScript = "Promise.all([document.fonts.ready, ...Array.from(document.images).map(image => image.decode())]).then(() => true, error => String(error && error.message ? error.message : error))";
23
+ export function slideGeometry(deviceClass, frame, captionPosition, layout = 'classic') {
24
+ return STORE_ART_LAYOUTS[layout].geometry({ deviceClass, frame, captionPosition });
25
+ }
26
+ const escapeHtml = (value) => value.replace(/[&<>"']/g, character => `&#${character.charCodeAt(0)};`);
27
+ const place = (box) => `left:${box.x}px;top:${box.y}px;width:${box.width}px;height:${box.height}px`;
28
+ const clamp = (lines) => `display:-webkit-box;-webkit-line-clamp:${lines};-webkit-box-orient:vertical;overflow:hidden`;
29
+ /**
30
+ * `locale` sayfanın `lang`'idir: CSS `text-transform: uppercase` dilin kuralını ondan alır — dil yokken Türkçe
31
+ * "bir" "BIR" çiziliyordu, "BİR" olmalı (2026-10-26, swiss stili).
32
+ */
33
+ export function slideHtml(geometry, style, slide, imageUrl, locale = 'en') {
34
+ const { canvas, caption, device, screen, cutout } = geometry;
35
+ const phone = device ?? screen;
36
+ const shadow = `0 ${Math.round(canvas.height * 0.012)}px ${Math.round(canvas.height * 0.03)}px rgba(0,0,0,${style.shadow})`;
37
+ const glow = style.glow ? `,0 0 ${Math.round(canvas.width * 0.09)}px ${style.colors.accent}88` : '';
38
+ const showScreen = screen !== undefined && imageUrl !== undefined;
39
+ return [
40
+ `<!doctype html><html lang="${escapeHtml(locale)}"><head><meta charset="utf-8"><style>`,
41
+ '*{margin:0;padding:0;box-sizing:border-box}',
42
+ `html,body{width:${canvas.width}px;height:${canvas.height}px;overflow:hidden}`,
43
+ `body{position:relative;background:${BACKDROPS[style.backdrop](style.colors, canvas)};color:${style.colors.text};font-family:${FONT_STACKS[style.font]};-webkit-font-smoothing:antialiased}`,
44
+ `.caption{position:absolute;${place(caption)};display:flex;flex-direction:column;justify-content:center;align-items:${style.align === 'left' ? 'flex-start' : 'center'};gap:${Math.round(geometry.subtitleSize * 0.5)}px;text-align:${style.align}}`,
45
+ `.eyebrow{font-size:${geometry.eyebrowSize}px;font-weight:700;letter-spacing:0.14em;text-transform:uppercase;color:${style.colors.accent}}`,
46
+ `.title{font-size:${geometry.titleSize}px;font-weight:${style.titleWeight};line-height:1.1;letter-spacing:-0.01em;text-transform:${style.titleCase};${clamp(geometry.titleLines)}}`,
47
+ `.subtitle{font-size:${geometry.subtitleSize}px;font-weight:500;line-height:1.25;opacity:0.8;${clamp(2)}}`,
48
+ phone ? `.phone{position:absolute;inset:0;transform-origin:${phone.x + phone.width / 2}px ${phone.y + phone.height / 2}px;transform:${geometry.phoneTransform ?? 'none'}}` : '',
49
+ device ? `.device{position:absolute;${place(device)};border-radius:${device.radius}px;background:${style.frameColor};box-shadow:${shadow}${glow},inset 0 0 0 ${Math.max(Math.round(device.width * 0.004), 2)}px #3a3a3e}` : '',
50
+ screen ? `.screen{position:absolute;${place(screen)};border-radius:${screen.radius}px;overflow:hidden;background:#ffffff${device ? '' : `;box-shadow:${shadow}${glow}`}}` : '',
51
+ '.screen img{display:block;width:100%;height:100%;object-fit:cover;object-position:top}',
52
+ cutout ? `.cutout{position:absolute;${place(cutout)};border-radius:${cutout.radius}px;background:#000000}` : '',
53
+ '</style></head><body>',
54
+ '<div class="caption">',
55
+ slide.eyebrow ? `<div class="eyebrow">${escapeHtml(slide.eyebrow)}</div>` : '',
56
+ `<div class="title">${escapeHtml(slide.title)}</div>`,
57
+ slide.subtitle ? `<div class="subtitle">${escapeHtml(slide.subtitle)}</div>` : '',
58
+ '</div>',
59
+ showScreen ? '<div class="phone">' : '',
60
+ showScreen && device ? '<div class="device"></div>' : '',
61
+ showScreen ? `<div class="screen"><img src="${escapeHtml(imageUrl)}" alt=""></div>` : '',
62
+ showScreen && cutout ? '<div class="cutout"></div>' : '',
63
+ showScreen ? '</div>' : '',
64
+ '</body></html>'
65
+ ].join('');
66
+ }
67
+ /** Kartın HTML'i; `imageUrl` ham karenin adresidir (çizimde file://, editör önizlemesinde galeri adresi) */
68
+ export function cardHtml(deck, spec, imageUrl) {
69
+ const raw = spec.raw === undefined || !STORE_ART_LAYOUTS[spec.layout].needsScreen ? undefined : deck.raws[spec.raw];
70
+ return slideHtml(slideGeometry(deck.deviceClass, spec.frame, spec.captionPosition, spec.layout), resolveStyle(spec.style, { primary: spec.accent, text: spec.text }), spec, raw ? imageUrl(raw) : undefined, deck.locale);
71
+ }
72
+ /**
73
+ * Kartları tek tarayıcıda sırayla çizer; her kart `fileName(card)` adıyla galeri dizinine yazılır. Editör geçici
74
+ * ad verir ve dosyaları ancak bütün iş başarılı olunca yerine taşır (yarıda düşen çizim kartı ezmesin).
75
+ */
76
+ export async function renderCards(launch, deck, directory, cards, fileName = cardFileName) {
77
+ const canvas = storeCanvas(deck.deviceClass);
78
+ const renderDirectory = path.join(directory, '.render');
79
+ let browser;
80
+ try {
81
+ await mkdir(renderDirectory, { recursive: true });
82
+ browser = await launch();
83
+ const context = await browser.newContext({ viewport: canvas, deviceScaleFactor: 1 });
84
+ try {
85
+ const page = await context.newPage();
86
+ const files = [];
87
+ for (const card of cards) {
88
+ const htmlPath = path.join(renderDirectory, `${card.name}.html`);
89
+ await writeFile(htmlPath, cardHtml(deck, card.spec, raw => pathToFileURL(raw.path).href));
90
+ await page.goto(pathToFileURL(htmlPath).href, { waitUntil: 'domcontentloaded', timeout: renderTimeoutMs });
91
+ const ready = await page.evaluate(slideReadyScript);
92
+ if (ready !== true) {
93
+ throw new PlatformApiError(`mobile_asset_preview: slides[${card.slide}] çizilemedi — ham kare tarayıcıda açılmadı (${String(ready)}). Dosyanın bozuk olmadığını page_screenshot ile yeniden çekerek doğrulayın (kart ${card.name}).`, 422);
94
+ }
95
+ const bytes = await page.screenshot({ type: 'jpeg', quality: jpegQuality, fullPage: false });
96
+ const size = imageSize(bytes);
97
+ if (size === undefined || size.width !== canvas.width || size.height !== canvas.height) {
98
+ throw new PlatformApiError(`mobile_asset_preview: slides[${card.slide}] ${size ? `${size.width}×${size.height}` : 'ölçüsü okunamayan'} çizildi, ${deck.deviceClass} mağaza ölçüsü ${canvas.width}×${canvas.height}. Mağaza bu kareyi reddederdi; dosya yazılmadı (kart ${card.name}).`, 500);
99
+ }
100
+ const file = path.join(directory, fileName(card));
101
+ await writeFile(file, bytes);
102
+ files.push({ card: card.name, style: card.spec.style, index: card.slide, path: file, width: size.width, height: size.height });
103
+ }
104
+ return files;
105
+ }
106
+ finally {
107
+ await context.close().catch(() => undefined);
108
+ }
109
+ }
110
+ finally {
111
+ await browser?.close().catch(() => undefined);
112
+ await rm(renderDirectory, { recursive: true, force: true });
113
+ }
114
+ }
@@ -0,0 +1,133 @@
1
+ import { copyFile, mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { z } from 'zod';
4
+ import { COLOR_PATTERN, SCREENSHOT_CAPTURE_DEVICES } from '../../knowledge/mobileStore.js';
5
+ import { PlatformApiError } from '../errors.js';
6
+ import { imageSize } from '../capture/capture.js';
7
+ import { LAYOUT_IDS, STORE_ART_LAYOUTS } from './layouts.js';
8
+ import { STYLE_IDS } from './styles.js';
9
+ /**
10
+ * Pazarlama görseli destesi (ADR 0004 §Ek): araçtan gelen kareler × stiller birer KART olur; galeri editörü kartın
11
+ * spec'ini değiştirip yeniden çizer. Ham kareler galeri dizinine `raw-NN.<uzantı>` adıyla kopyalanır — sunucu
12
+ * yalnız bu adları sunar, çizici de kopyayı okur. Spec'in sınırları aracın girdi sınırlarıyla aynıdır (araç bu
13
+ * sabitleri kullanır); editörden gelen spec sunucuda bu şemayla yeniden doğrulanır.
14
+ */
15
+ export const TITLE_MAX = 80;
16
+ export const SUBTITLE_MAX = 140;
17
+ export const EYEBROW_MAX = 30;
18
+ export const MAX_STYLES = 4;
19
+ export const MAX_CARDS = 40;
20
+ const defaultStyles = ['clean'];
21
+ const aspectTolerance = 0.01;
22
+ const pad = (value) => String(value).padStart(2, '0');
23
+ /** `deck.json`'a ve istemciye tanımsız alan gitmez: boş üst etiket "alan yok" demektir */
24
+ const withoutUndefined = (value) => Object.fromEntries(Object.entries(value).filter(([, field]) => field !== undefined));
25
+ /** Boş metin alanı siler: editörde üst etiketi ya da alt başlığı temizlemek alanı kaldırmaktır */
26
+ const optionalText = (max) => z.string().trim().max(max).optional().transform(value => value ? value : undefined);
27
+ const hexColor = z.string().regex(COLOR_PATTERN);
28
+ const cardSpecSchema = z.object({
29
+ raw: z.number().int().min(0).optional(),
30
+ eyebrow: optionalText(EYEBROW_MAX),
31
+ title: z.string().trim().min(1).max(TITLE_MAX),
32
+ subtitle: optionalText(SUBTITLE_MAX),
33
+ style: z.enum(STYLE_IDS),
34
+ layout: z.enum(LAYOUT_IDS),
35
+ captionPosition: z.enum(['top', 'bottom']),
36
+ accent: hexColor.optional(),
37
+ text: hexColor.optional(),
38
+ frame: z.boolean()
39
+ }).strict();
40
+ /** Editörden gelen spec: şema + destenin kendi kuralları (ham kare destede olmalı; cihaz gösteren yerleşim kare ister) */
41
+ export function parseCardSpec(deck, value) {
42
+ const parsed = cardSpecSchema.safeParse(value);
43
+ if (!parsed.success)
44
+ return { error: parsed.error.issues.map(issue => `${issue.path.join('.') || 'spec'}: ${issue.message}`).join('; ') };
45
+ const spec = withoutUndefined(parsed.data);
46
+ if (spec.raw !== undefined && spec.raw >= deck.raws.length)
47
+ return { error: `raw: destede ${deck.raws.length} ham kare var, ${spec.raw} yok` };
48
+ if (STORE_ART_LAYOUTS[spec.layout].needsScreen && spec.raw === undefined)
49
+ return { error: `layout: '${spec.layout}' ham kare ister — raw verin ya da 'text' seçin` };
50
+ return { spec };
51
+ }
52
+ async function readSlideSource(source, index) {
53
+ const absolute = path.resolve(source);
54
+ let bytes;
55
+ try {
56
+ bytes = await readFile(absolute);
57
+ }
58
+ catch (error) {
59
+ throw new PlatformApiError(`mobile_asset_preview: slides[${index}].path okunamadı — ${absolute} (${error instanceof Error ? error.message : String(error)}). Yol gateway sürecinin çalıştığı makineye göre çözülür; page_screenshot outPath'ini verin. Hiçbir kare çizilmedi.`, 400);
60
+ }
61
+ const size = imageSize(bytes);
62
+ if (size === undefined) {
63
+ throw new PlatformApiError(`mobile_asset_preview: slides[${index}].path PNG ya da JPEG değil — ${absolute}. Ham kareyi page_screenshot { format: 'jpeg', outPath } ile üretin. Hiçbir kare çizilmedi.`, 400);
64
+ }
65
+ return { absolute, size, png: bytes[0] === 0x89 };
66
+ }
67
+ /**
68
+ * Araç girdisinden desteyi kurar: ham kareleri doğrular ve galeri dizinine kopyalar, kartları kare sırasıyla
69
+ * (her karenin stilleri yan yana) dizer — deste sırası yükleme sırasıdır.
70
+ */
71
+ export async function buildDeck(input, directory) {
72
+ const styles = input.styles ?? defaultStyles;
73
+ if (new Set(styles).size !== styles.length) {
74
+ throw new PlatformApiError(`mobile_asset_preview: styles tekrar ediyor (${styles.join(', ')}) — her stil bir kez verilir. Hiçbir kare çizilmedi.`, 400);
75
+ }
76
+ const capture = SCREENSHOT_CAPTURE_DEVICES[input.deviceClass];
77
+ const sources = await Promise.all(input.slides.map(async (slide, index) => {
78
+ const layout = slide.layout ?? 'classic';
79
+ if (!STORE_ART_LAYOUTS[layout].needsScreen)
80
+ return undefined;
81
+ if (slide.path === undefined) {
82
+ throw new PlatformApiError(`mobile_asset_preview: slides[${index}].path gerekli — '${layout}' yerleşimi ham kareyi cihazda gösterir; yalnız 'text' yerleşimi kare istemez. Hiçbir kare çizilmedi.`, 400);
83
+ }
84
+ return readSlideSource(slide.path, index);
85
+ }));
86
+ // Oranı farklı kare ekrana `object-fit: cover` ile kırpılarak oturur; çizim durmaz ama ajan bilmeli.
87
+ const warnings = sources.flatMap((source, index) => source && Math.abs(source.size.height / source.size.width - capture.height / capture.width) > aspectTolerance
88
+ ? [`slides[${index}]: ham kare ${source.size.width}×${source.size.height}, ${input.deviceClass} ekranı ${capture.width}×${capture.height} oranında — kare ekrana kırpılarak oturdu. page_screenshot'ı device { width: ${capture.width}, height: ${capture.height}, scale: ${capture.scale} } ile çekin.`]
89
+ : []);
90
+ const raws = [];
91
+ const rawOfSlide = [];
92
+ await mkdir(directory, { recursive: true });
93
+ for (const [index, source] of sources.entries()) {
94
+ if (source === undefined) {
95
+ rawOfSlide.push(undefined);
96
+ continue;
97
+ }
98
+ const name = `raw-${pad(index + 1)}.${source.png ? 'png' : 'jpg'}`;
99
+ const copy = path.join(directory, name);
100
+ await copyFile(source.absolute, copy);
101
+ raws.push({ name, path: copy, ...source.size });
102
+ rawOfSlide.push(raws.length - 1);
103
+ }
104
+ const cards = input.slides.flatMap((slide, index) => styles.map((style) => ({
105
+ name: `${style}-${pad(index + 1)}`,
106
+ slide: index,
107
+ version: 1,
108
+ // Araç girdisi zod'dan geçmiştir; yalnız tanımsız alanlar atılır.
109
+ spec: withoutUndefined({
110
+ raw: rawOfSlide[index],
111
+ eyebrow: slide.eyebrow,
112
+ title: slide.title,
113
+ subtitle: slide.subtitle,
114
+ style,
115
+ layout: slide.layout ?? 'classic',
116
+ captionPosition: input.captionPosition ?? 'top',
117
+ accent: input.palette?.primary,
118
+ text: input.palette?.text,
119
+ frame: input.frame ?? true
120
+ })
121
+ })));
122
+ return { deck: { deviceClass: input.deviceClass, locale: input.locale, slot: `screenshot/${input.locale}/${input.deviceClass}`, raws, cards }, warnings };
123
+ }
124
+ /** Kartın kopyası: aynı spec, kare sırası ve dosya; ad `<ad>-2`, `<ad>-3` … */
125
+ export function duplicateCard(deck, original) {
126
+ const names = new Set(deck.cards.map(card => card.name));
127
+ let suffix = 2;
128
+ while (names.has(`${original.name}-${suffix}`))
129
+ suffix += 1;
130
+ return { name: `${original.name}-${suffix}`, slide: original.slide, version: 1, spec: { ...original.spec } };
131
+ }
132
+ export const cardFileName = (card) => `${card.name}.jpg`;
133
+ export const saveDeck = (directory, deck) => writeFile(path.join(directory, 'deck.json'), JSON.stringify(deck, null, 2));