docs-combiner 0.2.2 → 1.0.0

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 (53) hide show
  1. package/dist/agentApi/agentControlClaim.js +26 -0
  2. package/dist/agentApi/approachMatrixView.js +19 -0
  3. package/dist/agentApi/capabilitiesMeta.js +78 -0
  4. package/dist/agentApi/catalogUrlPresets.js +26 -0
  5. package/dist/agentApi/constants.js +11 -0
  6. package/dist/agentApi/creativeSelections.js +25 -0
  7. package/dist/agentApi/escalation.js +221 -0
  8. package/dist/agentApi/generatedPairsGuard.js +85 -0
  9. package/dist/agentApi/httpHelpers.js +77 -0
  10. package/dist/agentApi/imagesReadiness.js +60 -0
  11. package/dist/agentApi/jobStatus.js +113 -0
  12. package/dist/agentApi/productImageDrive.js +39 -0
  13. package/dist/agentApi/regenerateImages.js +125 -0
  14. package/dist/agentApi/rendererTypes.js +3 -0
  15. package/dist/agentApi/routes.js +440 -0
  16. package/dist/agentApi/sessionSnapshot.js +82 -0
  17. package/dist/agentApi/sessionTypes.js +2 -0
  18. package/dist/agentApi/spec.js +1045 -0
  19. package/dist/agentApi/types.js +2 -0
  20. package/dist/agentApi/validation.js +32 -0
  21. package/dist/campaignsCsv.js +420 -0
  22. package/dist/contentPairs.js +62 -0
  23. package/dist/flexcardBalance.js +88 -0
  24. package/dist/integrations/driveApi.js +420 -0
  25. package/dist/integrations/httpTimeout.js +116 -0
  26. package/dist/integrations/openrouter.js +532 -0
  27. package/dist/main.js +830 -165
  28. package/dist/models.js +17 -0
  29. package/dist/offerSettings.js +19 -0
  30. package/dist/preload.js +28 -0
  31. package/dist/promptOverrides.js +437 -0
  32. package/dist/prompts.js +1243 -0
  33. package/dist/renderer.js +19 -19
  34. package/dist/renderer.js.LICENSE.txt +4 -0
  35. package/dist/renderer.js.map +1 -1
  36. package/dist/server/app.js +378 -0
  37. package/dist/server/auth.js +74 -0
  38. package/dist/server/contentJobs.js +367 -0
  39. package/dist/server/driveCatalog.js +122 -0
  40. package/dist/server/driveProxy.js +220 -0
  41. package/dist/server/flexcardMeta.js +29 -0
  42. package/dist/server/googleAuth.js +84 -0
  43. package/dist/server/imageAssets.js +87 -0
  44. package/dist/server/imageJobs.js +776 -0
  45. package/dist/server/index.js +38 -0
  46. package/dist/server/jobEvents.js +116 -0
  47. package/dist/server/jobs.js +358 -0
  48. package/dist/server/openRouterMeta.js +58 -0
  49. package/dist/server/spec.js +544 -0
  50. package/dist/server/storage.js +133 -0
  51. package/dist/server/telegram.js +53 -0
  52. package/dist/server/workspaceSnapshot.js +273 -0
  53. package/package.json +18 -4
@@ -0,0 +1,776 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.normalizeCreateImageJobPayload = normalizeCreateImageJobPayload;
13
+ exports.runServerImageJob = runServerImageJob;
14
+ exports.createServerImageJob = createServerImageJob;
15
+ const openrouter_1 = require("../integrations/openrouter");
16
+ const driveApi_1 = require("../integrations/driveApi");
17
+ const models_1 = require("../models");
18
+ const prompts_1 = require("../prompts");
19
+ const imageAssets_1 = require("./imageAssets");
20
+ const jobs_1 = require("./jobs");
21
+ const googleAuth_1 = require("./googleAuth");
22
+ const DEFAULT_SLOT_CONCURRENCY = 12;
23
+ const MAX_SLOT_CONCURRENCY = 12;
24
+ function logImageJob(jobId, message, meta) {
25
+ const suffix = meta ? ` ${JSON.stringify(meta)}` : '';
26
+ console.log(`[docs-combiner-server] image-job ${jobId} ${message}${suffix}`);
27
+ }
28
+ /** Достаёт width/height из data-URL (PNG/JPEG/WebP) без внешних зависимостей — для дебага реальных пропорций генерации. */
29
+ function readImageDimensionsFromDataUrl(dataUrl) {
30
+ const m = /^data:[^;,]*;base64,([\s\S]*)$/.exec(dataUrl);
31
+ if (!m)
32
+ return null;
33
+ let buf;
34
+ try {
35
+ buf = Buffer.from(m[1], 'base64');
36
+ }
37
+ catch (_a) {
38
+ return null;
39
+ }
40
+ // PNG: сигнатура + IHDR (width @16, height @20, big-endian)
41
+ if (buf.length >= 24 && buf.toString('ascii', 1, 4) === 'PNG') {
42
+ return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20) };
43
+ }
44
+ // JPEG: ищем SOF-маркер
45
+ if (buf.length >= 4 && buf[0] === 0xff && buf[1] === 0xd8) {
46
+ let offset = 2;
47
+ while (offset + 9 < buf.length) {
48
+ if (buf[offset] !== 0xff) {
49
+ offset += 1;
50
+ continue;
51
+ }
52
+ const marker = buf[offset + 1];
53
+ const isSof = (marker >= 0xc0 && marker <= 0xc3) ||
54
+ (marker >= 0xc5 && marker <= 0xc7) ||
55
+ (marker >= 0xc9 && marker <= 0xcb) ||
56
+ (marker >= 0xcd && marker <= 0xcf);
57
+ if (isSof) {
58
+ return { height: buf.readUInt16BE(offset + 5), width: buf.readUInt16BE(offset + 7) };
59
+ }
60
+ const segmentLength = buf.readUInt16BE(offset + 2);
61
+ offset += 2 + segmentLength;
62
+ }
63
+ return null;
64
+ }
65
+ // WebP: RIFF....WEBP
66
+ if (buf.length >= 30 && buf.toString('ascii', 0, 4) === 'RIFF' && buf.toString('ascii', 8, 12) === 'WEBP') {
67
+ const format = buf.toString('ascii', 12, 16);
68
+ if (format === 'VP8X') {
69
+ const width = 1 + ((buf[24] | (buf[25] << 8) | (buf[26] << 16)) & 0xffffff);
70
+ const height = 1 + ((buf[27] | (buf[28] << 8) | (buf[29] << 16)) & 0xffffff);
71
+ return { width, height };
72
+ }
73
+ if (format === 'VP8 ') {
74
+ const width = buf.readUInt16LE(26) & 0x3fff;
75
+ const height = buf.readUInt16LE(28) & 0x3fff;
76
+ return { width, height };
77
+ }
78
+ if (format === 'VP8L') {
79
+ const b = buf.subarray(21, 25);
80
+ const bits = b[0] | (b[1] << 8) | (b[2] << 16) | (b[3] << 24);
81
+ const width = (bits & 0x3fff) + 1;
82
+ const height = ((bits >> 14) & 0x3fff) + 1;
83
+ return { width, height };
84
+ }
85
+ }
86
+ return null;
87
+ }
88
+ function isPlainObject(value) {
89
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
90
+ }
91
+ function badRequest(message) {
92
+ const err = new Error(message);
93
+ err.status = 400;
94
+ return err;
95
+ }
96
+ function normalizeAspectRatio(value) {
97
+ return value === '1:1' || value === '2:3' || value === '4:5' ? value : undefined;
98
+ }
99
+ function normalizeImageSize(value) {
100
+ return value === '1K' || value === '2K' || value === '4K' ? value : undefined;
101
+ }
102
+ function normalizeStringArray(value) {
103
+ if (!Array.isArray(value))
104
+ return undefined;
105
+ const items = value.map(item => (typeof item === 'string' ? item.trim() : '')).filter(Boolean);
106
+ return items.length ? items : undefined;
107
+ }
108
+ function normalizeReferenceImageJobSlots(value) {
109
+ if (!Array.isArray(value))
110
+ return undefined;
111
+ const items = value
112
+ .map(item => {
113
+ if (!isPlainObject(item))
114
+ return null;
115
+ const jobId = typeof item.jobId === 'string' ? item.jobId.trim() : '';
116
+ const slotIndex = Number(item.slotIndex);
117
+ if (!jobId || !Number.isInteger(slotIndex) || slotIndex < 1)
118
+ return null;
119
+ return { jobId, slotIndex };
120
+ })
121
+ .filter((item) => Boolean(item));
122
+ return items.length ? items : undefined;
123
+ }
124
+ function normalizeReferenceImageJobSlot(value) {
125
+ if (!isPlainObject(value))
126
+ return undefined;
127
+ const jobId = typeof value.jobId === 'string' ? value.jobId.trim() : '';
128
+ const slotIndex = Number(value.slotIndex);
129
+ if (!jobId || !Number.isInteger(slotIndex) || slotIndex < 1)
130
+ return undefined;
131
+ return { jobId, slotIndex };
132
+ }
133
+ function normalizeAssetIdArray(value) {
134
+ if (!Array.isArray(value))
135
+ return undefined;
136
+ const items = value.map(item => (typeof item === 'string' ? item.trim() : '')).filter(Boolean);
137
+ return items.length ? items : undefined;
138
+ }
139
+ function normalizeValidationInput(value) {
140
+ if (!isPlainObject(value))
141
+ return undefined;
142
+ const product = typeof value.product === 'string' ? value.product.trim() : '';
143
+ const geo = typeof value.geo === 'string' ? value.geo.trim() : '';
144
+ if (!product || !geo)
145
+ return undefined;
146
+ return {
147
+ product,
148
+ geo,
149
+ approachName: typeof value.approachName === 'string' ? value.approachName.trim() : undefined,
150
+ productReferenceUrl: typeof value.productReferenceUrl === 'string' ? value.productReferenceUrl.trim() : undefined,
151
+ priceBrief: typeof value.priceBrief === 'string' ? value.priceBrief.trim() : undefined,
152
+ model: typeof value.model === 'string' ? value.model.trim() : undefined,
153
+ disabled: value.disabled === true,
154
+ expectNoBullets: typeof value.expectNoBullets === 'boolean' ? value.expectNoBullets : undefined,
155
+ };
156
+ }
157
+ function normalizeUpload(value, fallbackFolderId) {
158
+ if (!isPlainObject(value))
159
+ return undefined;
160
+ const enabled = value.enabled === true;
161
+ if (!enabled)
162
+ return undefined;
163
+ const folderId = typeof value.folderId === 'string' && value.folderId.trim()
164
+ ? value.folderId.trim()
165
+ : fallbackFolderId;
166
+ return {
167
+ enabled,
168
+ folderId,
169
+ filename: typeof value.filename === 'string' && value.filename.trim() ? value.filename.trim() : undefined,
170
+ };
171
+ }
172
+ function normalizeSlot(slot, i, defaults) {
173
+ var _a, _b, _c, _d, _e, _f;
174
+ const item = isPlainObject(slot) ? slot : {};
175
+ return {
176
+ index: Number.isInteger(item.index) && Number(item.index) > 0 ? Number(item.index) : i + 1,
177
+ prompt: typeof item.prompt === 'string' ? item.prompt : undefined,
178
+ approach: typeof item.approach === 'string' ? item.approach : undefined,
179
+ aspectRatio: normalizeAspectRatio(item.aspectRatio),
180
+ imageSize: normalizeImageSize(item.imageSize),
181
+ model: typeof item.model === 'string' && item.model.trim() ? item.model.trim() : defaults.model,
182
+ sourceImageAssetId: typeof item.sourceImageAssetId === 'string' && item.sourceImageAssetId.trim() ? item.sourceImageAssetId.trim() : undefined,
183
+ sourceImageUrl: typeof item.sourceImageUrl === 'string' && item.sourceImageUrl.trim() ? item.sourceImageUrl.trim() : undefined,
184
+ sourceImageJobSlot: normalizeReferenceImageJobSlot(item.sourceImageJobSlot),
185
+ referenceImageAssetIds: (_a = normalizeAssetIdArray(item.referenceImageAssetIds)) !== null && _a !== void 0 ? _a : defaults.referenceImageAssetIds,
186
+ referenceImageUrls: (_b = normalizeStringArray(item.referenceImageUrls)) !== null && _b !== void 0 ? _b : defaults.referenceImageUrls,
187
+ referenceImageJobSlots: (_c = normalizeReferenceImageJobSlots(item.referenceImageJobSlots)) !== null && _c !== void 0 ? _c : defaults.referenceImageJobSlots,
188
+ validationInput: (_d = normalizeValidationInput(item.validation)) !== null && _d !== void 0 ? _d : defaults.validationInput,
189
+ upload: (_f = normalizeUpload(item.upload, ((_e = defaults.upload) === null || _e === void 0 ? void 0 : _e.folderId) || '')) !== null && _f !== void 0 ? _f : defaults.upload,
190
+ };
191
+ }
192
+ function normalizeCreateImageJobPayload(payload) {
193
+ const workspaceId = typeof payload.workspaceId === 'string' ? payload.workspaceId.trim() : '';
194
+ if (!workspaceId)
195
+ throw badRequest('workspaceId is required.');
196
+ const defaultModel = typeof payload.model === 'string' && payload.model.trim() ? payload.model.trim() : undefined;
197
+ const defaultReferenceImageAssetIds = normalizeAssetIdArray(payload.referenceImageAssetIds);
198
+ const defaultReferenceImageUrls = normalizeStringArray(payload.referenceImageUrls);
199
+ const defaultReferenceImageJobSlots = normalizeReferenceImageJobSlots(payload.referenceImageJobSlots);
200
+ const defaultValidationInput = normalizeValidationInput(payload.validation);
201
+ const defaultUpload = normalizeUpload(payload.upload, workspaceId);
202
+ const input = {
203
+ workspaceId,
204
+ metadata: isPlainObject(payload.metadata) ? payload.metadata : {},
205
+ };
206
+ if (Array.isArray(payload.slots)) {
207
+ input.slots = payload.slots.map((slot, i) => normalizeSlot(slot, i, {
208
+ model: defaultModel,
209
+ referenceImageAssetIds: defaultReferenceImageAssetIds,
210
+ referenceImageUrls: defaultReferenceImageUrls,
211
+ referenceImageJobSlots: defaultReferenceImageJobSlots,
212
+ validationInput: defaultValidationInput,
213
+ upload: defaultUpload,
214
+ }));
215
+ }
216
+ else if (payload.count !== undefined) {
217
+ const count = Number(payload.count);
218
+ if (!Number.isFinite(count) || count < 1)
219
+ throw badRequest('count must be a positive number.');
220
+ input.count = Math.floor(count);
221
+ input.slots = Array.from({ length: input.count }, (_, i) => normalizeSlot({}, i, {
222
+ model: defaultModel,
223
+ referenceImageAssetIds: defaultReferenceImageAssetIds,
224
+ referenceImageUrls: defaultReferenceImageUrls,
225
+ referenceImageJobSlots: defaultReferenceImageJobSlots,
226
+ validationInput: defaultValidationInput,
227
+ upload: defaultUpload,
228
+ }));
229
+ }
230
+ return input;
231
+ }
232
+ function toVisionApiImageUrl(url) {
233
+ const fileIdMatch = url.match(/\/file\/d\/([a-zA-Z0-9_-]+)/);
234
+ if (fileIdMatch === null || fileIdMatch === void 0 ? void 0 : fileIdMatch[1]) {
235
+ return `https://drive.google.com/uc?export=view&id=${fileIdMatch[1]}`;
236
+ }
237
+ return url;
238
+ }
239
+ function imageConfigFor(model, aspectRatio, imageSize) {
240
+ const isGptImageModel = model.includes('gpt-5-image') || model.includes('gpt-image-1');
241
+ if (isGptImageModel) {
242
+ // gpt-image-1 не поддерживает 4:5 нативно → ближайший портрет 1024x1536; image_size не применяется.
243
+ return { size: aspectRatio === '1:1' ? '1024x1024' : '1024x1536' };
244
+ }
245
+ const config = { aspect_ratio: aspectRatio };
246
+ if (imageSize)
247
+ config.image_size = imageSize;
248
+ return config;
249
+ }
250
+ function resolveReferenceImageJobSlotUrls(registry, assetStore, refs) {
251
+ if (!(refs === null || refs === void 0 ? void 0 : refs.length))
252
+ return [];
253
+ return refs.map(ref => {
254
+ const sourceJob = registry.getJob(ref.jobId);
255
+ if ((sourceJob === null || sourceJob === void 0 ? void 0 : sourceJob.kind) !== 'images') {
256
+ throw new Error(`Reference image job not found in server memory: ${ref.jobId}.`);
257
+ }
258
+ const sourceSlot = sourceJob === null || sourceJob === void 0 ? void 0 : sourceJob.slots.find(slot => slot.index === ref.slotIndex);
259
+ const imageDataUrl = sourceSlot === null || sourceSlot === void 0 ? void 0 : sourceSlot.imageDataUrl;
260
+ if (!imageDataUrl) {
261
+ throw new Error(`Reference image not found in server memory: ${ref.jobId} slot ${ref.slotIndex}.`);
262
+ }
263
+ return imageDataUrl;
264
+ });
265
+ }
266
+ function resolveReferenceImageAssetUrls(assetStore, assetIds) {
267
+ if (!(assetIds === null || assetIds === void 0 ? void 0 : assetIds.length))
268
+ return [];
269
+ return assetIds.map(assetId => {
270
+ const asset = assetStore.get(assetId);
271
+ if (!asset)
272
+ throw new Error(`Reference image asset not found in server memory: ${assetId}.`);
273
+ return asset.imageDataUrl;
274
+ });
275
+ }
276
+ function resolveSourceImageUrl(registry, assetStore, slot) {
277
+ var _a, _b;
278
+ if ((_a = slot.sourceImageAssetId) === null || _a === void 0 ? void 0 : _a.trim()) {
279
+ const asset = assetStore.get(slot.sourceImageAssetId.trim());
280
+ if (!asset)
281
+ throw new Error(`Source image asset not found in server memory: ${slot.sourceImageAssetId}.`);
282
+ return asset.imageDataUrl;
283
+ }
284
+ if ((_b = slot.sourceImageUrl) === null || _b === void 0 ? void 0 : _b.trim())
285
+ return slot.sourceImageUrl.trim();
286
+ const ref = slot.sourceImageJobSlot;
287
+ if (!ref)
288
+ return null;
289
+ const sourceJob = registry.getJob(ref.jobId);
290
+ if ((sourceJob === null || sourceJob === void 0 ? void 0 : sourceJob.kind) !== 'images') {
291
+ throw new Error(`Source image job not found in server memory: ${ref.jobId}.`);
292
+ }
293
+ const sourceSlot = sourceJob === null || sourceJob === void 0 ? void 0 : sourceJob.slots.find(item => item.index === ref.slotIndex);
294
+ const imageDataUrl = sourceSlot === null || sourceSlot === void 0 ? void 0 : sourceSlot.imageDataUrl;
295
+ if (!imageDataUrl) {
296
+ throw new Error(`Source image not found in server memory: ${ref.jobId} slot ${ref.slotIndex}.`);
297
+ }
298
+ return imageDataUrl;
299
+ }
300
+ function buildOpenRouterImageRequest(slot, fallbackModel, memoryReferenceImageUrls = []) {
301
+ var _a, _b, _c, _d;
302
+ const prompt = (_a = slot.prompt) === null || _a === void 0 ? void 0 : _a.trim();
303
+ if (!prompt)
304
+ throw badRequest(`Slot ${slot.index}: prompt is required for server-side image generation.`);
305
+ const model = ((_b = slot.model) === null || _b === void 0 ? void 0 : _b.trim()) || fallbackModel;
306
+ const aspectRatio = (_c = slot.aspectRatio) !== null && _c !== void 0 ? _c : '1:1';
307
+ const referenceImageUrls = [
308
+ ...memoryReferenceImageUrls,
309
+ ...((_d = slot.referenceImageUrls) !== null && _d !== void 0 ? _d : []).map(toVisionApiImageUrl),
310
+ ];
311
+ const content = [{ type: 'text', text: prompt }];
312
+ for (const url of referenceImageUrls) {
313
+ content.push({ type: 'image_url', image_url: { url } });
314
+ }
315
+ return {
316
+ model,
317
+ messages: [
318
+ {
319
+ role: 'user',
320
+ content: referenceImageUrls.length ? content : prompt,
321
+ },
322
+ ],
323
+ modalities: ['image', 'text'],
324
+ reasoning: { effort: 'low' },
325
+ image_config: imageConfigFor(model, aspectRatio, slot.imageSize),
326
+ };
327
+ }
328
+ function extractImageUrlFromPart(part) {
329
+ var _a, _b;
330
+ const candidates = [
331
+ (_a = part === null || part === void 0 ? void 0 : part.image_url) === null || _a === void 0 ? void 0 : _a.url,
332
+ part === null || part === void 0 ? void 0 : part.image_url,
333
+ part === null || part === void 0 ? void 0 : part.url,
334
+ (_b = part === null || part === void 0 ? void 0 : part.source) === null || _b === void 0 ? void 0 : _b.data,
335
+ ];
336
+ const found = candidates.find(value => typeof value === 'string' && value.trim());
337
+ return typeof found === 'string' ? found.trim() : null;
338
+ }
339
+ function summarizeOpenRouterImageResponse(data) {
340
+ var _a, _b, _c, _d, _e, _f;
341
+ const message = (_b = (_a = data === null || data === void 0 ? void 0 : data.choices) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.message;
342
+ const content = message === null || message === void 0 ? void 0 : message.content;
343
+ let contentPreview = '';
344
+ if (typeof content === 'string') {
345
+ contentPreview = content;
346
+ }
347
+ else if (Array.isArray(content)) {
348
+ contentPreview = content
349
+ .map((part) => {
350
+ if (typeof part === 'string')
351
+ return part;
352
+ if (typeof (part === null || part === void 0 ? void 0 : part.text) === 'string')
353
+ return part.text;
354
+ if (typeof (part === null || part === void 0 ? void 0 : part.content) === 'string')
355
+ return part.content;
356
+ if (part === null || part === void 0 ? void 0 : part.type)
357
+ return `[${part.type}]`;
358
+ return '';
359
+ })
360
+ .filter(Boolean)
361
+ .join('\n');
362
+ }
363
+ const summary = {
364
+ finishReason: (_d = (_c = data === null || data === void 0 ? void 0 : data.choices) === null || _c === void 0 ? void 0 : _c[0]) === null || _d === void 0 ? void 0 : _d.finish_reason,
365
+ nativeFinishReason: (_f = (_e = data === null || data === void 0 ? void 0 : data.choices) === null || _e === void 0 ? void 0 : _e[0]) === null || _f === void 0 ? void 0 : _f.native_finish_reason,
366
+ messageKeys: message && typeof message === 'object' ? Object.keys(message) : [],
367
+ contentPreview: contentPreview.slice(0, 600),
368
+ };
369
+ return JSON.stringify(summary);
370
+ }
371
+ function extractGeneratedImageUrl(data) {
372
+ var _a, _b, _c, _d, _e, _f;
373
+ const choiceError = (_b = (_a = data === null || data === void 0 ? void 0 : data.choices) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.error;
374
+ if (choiceError) {
375
+ throw new Error(`Provider error ${(_c = choiceError.code) !== null && _c !== void 0 ? _c : ''}: ${(_d = choiceError.message) !== null && _d !== void 0 ? _d : 'Image generation failed'}`);
376
+ }
377
+ const message = (_f = (_e = data === null || data === void 0 ? void 0 : data.choices) === null || _e === void 0 ? void 0 : _e[0]) === null || _f === void 0 ? void 0 : _f.message;
378
+ if (message === null || message === void 0 ? void 0 : message.refusal)
379
+ throw new Error(`Image generation refused: ${message.refusal}`);
380
+ const imageUrlFromImages = Array.isArray(message === null || message === void 0 ? void 0 : message.images)
381
+ ? message.images.map(extractImageUrlFromPart).find(Boolean)
382
+ : null;
383
+ if (imageUrlFromImages)
384
+ return imageUrlFromImages;
385
+ const content = message === null || message === void 0 ? void 0 : message.content;
386
+ const imageUrlFromContent = Array.isArray(content)
387
+ ? content.map(extractImageUrlFromPart).find(Boolean)
388
+ : extractImageUrlFromPart(content);
389
+ if (imageUrlFromContent)
390
+ return imageUrlFromContent;
391
+ throw new Error(`No image found in OpenRouter response. ${summarizeOpenRouterImageResponse(data)}`);
392
+ }
393
+ function generateSlotImage(args) {
394
+ return __awaiter(this, void 0, void 0, function* () {
395
+ var _a;
396
+ const requestBody = buildOpenRouterImageRequest(args.slot, args.fallbackModel, args.memoryReferenceImageUrls);
397
+ const timeoutController = new AbortController();
398
+ const onAbort = () => timeoutController.abort(args.signal.reason);
399
+ args.signal.addEventListener('abort', onAbort, { once: true });
400
+ const timeoutId = setTimeout(() => timeoutController.abort(), args.timeoutMs);
401
+ try {
402
+ const response = yield (0, openrouter_1.postOpenRouterChatCompletion)(requestBody, {
403
+ apiKey: args.apiKey,
404
+ signal: timeoutController.signal,
405
+ });
406
+ const responseText = yield response.text();
407
+ if (!response.ok) {
408
+ let message = `HTTP ${response.status}: ${response.statusText}`;
409
+ try {
410
+ const parsed = JSON.parse(responseText);
411
+ message = ((_a = parsed.error) === null || _a === void 0 ? void 0 : _a.message) || parsed.message || message;
412
+ }
413
+ catch (_b) {
414
+ if (responseText.trim())
415
+ message = `${message}. Response: ${responseText.slice(0, 200)}`;
416
+ }
417
+ throw new Error(message);
418
+ }
419
+ const data = JSON.parse(responseText);
420
+ return {
421
+ imageUrl: extractGeneratedImageUrl(data),
422
+ model: String(requestBody.model),
423
+ };
424
+ }
425
+ catch (err) {
426
+ if ((err === null || err === void 0 ? void 0 : err.name) === 'AbortError')
427
+ throw new Error('Image generation aborted or timed out.');
428
+ throw err;
429
+ }
430
+ finally {
431
+ clearTimeout(timeoutId);
432
+ args.signal.removeEventListener('abort', onAbort);
433
+ }
434
+ });
435
+ }
436
+ function extractValidationErrors(content) {
437
+ const lines = content.split('\n').map(line => line.trim()).filter(Boolean);
438
+ const labeled = lines
439
+ .map(line => {
440
+ var _a;
441
+ const match = line.match(/^(?:ОШИБКА|ERROR)\s*[::]\s*(.+)$/i);
442
+ return ((_a = match === null || match === void 0 ? void 0 : match[1]) === null || _a === void 0 ? void 0 : _a.trim()) || '';
443
+ })
444
+ .filter(Boolean)
445
+ .filter(line => !/^(нет|none|no errors?)$/i.test(line));
446
+ return [...new Set(labeled)];
447
+ }
448
+ function isMissingBulletsValidationError(value) {
449
+ const text = value.toLowerCase();
450
+ return (/буллет|bullet/.test(text) ||
451
+ (/выгод/.test(text) && /икон|галоч|блок|спис/.test(text)) ||
452
+ (/benefit/.test(text) && /icon|check|bullet|list/.test(text)));
453
+ }
454
+ function validateSlotImage(args) {
455
+ return __awaiter(this, void 0, void 0, function* () {
456
+ var _a, _b, _c, _d, _e;
457
+ const input = args.slot.validationInput;
458
+ if (!input) {
459
+ throw new Error(`Slot ${args.slot.index}: validation input is required.`);
460
+ }
461
+ if (input.disabled) {
462
+ return { status: 'ok', result: 'Проверка отключена на сервере', errors: [] };
463
+ }
464
+ const referenceUrl = (_a = input.productReferenceUrl) === null || _a === void 0 ? void 0 : _a.trim();
465
+ const validationPrompt = (referenceUrl ? `${(0, prompts_1.getValidationProductReferencePreamble)()}\n` : '') +
466
+ (0, prompts_1.getValidationPrompt)(input.product, input.geo, [], input.approachName || args.slot.approach, true, input.priceBrief, { expectNoBullets: input.expectNoBullets });
467
+ const content = [
468
+ { type: 'text', text: validationPrompt },
469
+ { type: 'image_url', image_url: { url: toVisionApiImageUrl(args.imageUrl) } },
470
+ ];
471
+ if (referenceUrl) {
472
+ content.push({ type: 'image_url', image_url: { url: toVisionApiImageUrl(referenceUrl) } });
473
+ }
474
+ const completion = yield (0, openrouter_1.requestOpenRouterChatCompletion)({
475
+ model: input.model || args.fallbackModel,
476
+ messages: [{ role: 'user', content }],
477
+ max_tokens: 16000,
478
+ }, {
479
+ apiKey: args.apiKey,
480
+ preferStream: false,
481
+ signal: args.signal,
482
+ readTimeoutMs: 120000,
483
+ });
484
+ const choice = (_c = (_b = completion.data) === null || _b === void 0 ? void 0 : _b.choices) === null || _c === void 0 ? void 0 : _c[0];
485
+ const responseContent = (0, openrouter_1.extractChatCompletionText)(choice) || '';
486
+ const statusMatchRu = responseContent.match(/СТАТУС\s*:\s*(OK|НУЖНА\s+ПЕРЕСБОРКА)/i);
487
+ const statusMatchEn = responseContent.match(/\bSTATUS\s*:\s*(OK|NEEDS?\s+REBUILD|FAIL|FAILED)\b/i);
488
+ const token = ((_e = (_d = statusMatchRu === null || statusMatchRu === void 0 ? void 0 : statusMatchRu[1]) !== null && _d !== void 0 ? _d : statusMatchEn === null || statusMatchEn === void 0 ? void 0 : statusMatchEn[1]) !== null && _e !== void 0 ? _e : '').toUpperCase();
489
+ let status = token === 'OK' ? 'ok' : 'needs_rebuild';
490
+ let errors = extractValidationErrors(responseContent);
491
+ if (input.expectNoBullets === true) {
492
+ const filteredErrors = errors.filter(error => !isMissingBulletsValidationError(error));
493
+ if (errors.length > 0 && filteredErrors.length === 0) {
494
+ status = 'ok';
495
+ }
496
+ errors = filteredErrors;
497
+ }
498
+ if ((0, openrouter_1.isCompletionTruncatedByTokenLimit)(choice)) {
499
+ errors = [
500
+ 'ОШИБКА: ответ валидатора обрезан по лимиту токенов. Повторите проверку или выберите модель без тяжёлого reasoning.',
501
+ ...errors,
502
+ ];
503
+ }
504
+ if (status === 'needs_rebuild' && errors.length === 0) {
505
+ errors = [responseContent.slice(0, 900) || 'Validator returned needs_rebuild without parsed errors.'];
506
+ }
507
+ return {
508
+ status,
509
+ result: (0, openrouter_1.buildValidatorStoredResultText)(responseContent, completion.data),
510
+ errors,
511
+ };
512
+ });
513
+ }
514
+ function dataUrlToBlob(dataUrl) {
515
+ const match = dataUrl.match(/^data:([^;,]+);base64,(.+)$/);
516
+ if (!match)
517
+ throw new Error('Only base64 data URLs can be uploaded to Drive.');
518
+ const mimeType = match[1] || 'image/png';
519
+ const buffer = Buffer.from(match[2], 'base64');
520
+ return { blob: new Blob([buffer], { type: mimeType }), mimeType };
521
+ }
522
+ function defaultServerImageFilename(slot) {
523
+ const suffixMap = { '2:3': '23', '4:5': '45' };
524
+ const suffix = suffixMap[slot.aspectRatio || ''] || '11';
525
+ return `server_${slot.index}_${Date.now().toString(36)}_${suffix}.png`;
526
+ }
527
+ function uploadSlotImageToDrive(args) {
528
+ return __awaiter(this, void 0, void 0, function* () {
529
+ var _a, _b, _c, _d;
530
+ const folderId = (_b = (_a = args.slot.upload) === null || _a === void 0 ? void 0 : _a.folderId) === null || _b === void 0 ? void 0 : _b.trim();
531
+ if (!folderId)
532
+ throw new Error(`Slot ${args.slot.index}: Drive folder id is required for upload.`);
533
+ const filename = ((_d = (_c = args.slot.upload) === null || _c === void 0 ? void 0 : _c.filename) === null || _d === void 0 ? void 0 : _d.trim()) || defaultServerImageFilename(args.slot);
534
+ const { blob } = dataUrlToBlob(args.imageUrl);
535
+ return (yield (0, googleAuth_1.withGoogleAccessTokenRetry)(args.store, (token) => __awaiter(this, void 0, void 0, function* () {
536
+ var _a;
537
+ const form = new FormData();
538
+ form.append('metadata', new Blob([JSON.stringify({
539
+ name: filename,
540
+ parents: [folderId],
541
+ })], { type: 'application/json' }));
542
+ form.append('file', blob, filename);
543
+ const response = yield (0, driveApi_1.driveMultipartUploadRequest)(token, form, { supportsAllDrives: true });
544
+ const data = yield response.json().catch(() => ({}));
545
+ if (!response.ok) {
546
+ const err = new Error(((_a = data === null || data === void 0 ? void 0 : data.error) === null || _a === void 0 ? void 0 : _a.message) || (data === null || data === void 0 ? void 0 : data.message) || `Drive upload failed (${response.status})`);
547
+ err.status = response.status;
548
+ throw err;
549
+ }
550
+ const fileId = typeof (data === null || data === void 0 ? void 0 : data.id) === 'string' ? data.id : '';
551
+ if (!fileId)
552
+ throw new Error('Drive upload did not return file id.');
553
+ return { status: response.status, value: fileId };
554
+ }))).value;
555
+ });
556
+ }
557
+ function runServerImageJob(store_1, registry_1, jobId_1) {
558
+ return __awaiter(this, arguments, void 0, function* (store, registry, jobId, options = {}) {
559
+ var _a, _b, _c, _d, _e, _f, _g, _h;
560
+ const config = yield store.readConfig();
561
+ const apiKey = (_a = config.openaiApiKey) === null || _a === void 0 ? void 0 : _a.trim();
562
+ if (!apiKey) {
563
+ return registry.markJobFailed(jobId, 'OpenRouter API key is not configured on the server.');
564
+ }
565
+ const settings = yield store.readSettings();
566
+ const fallbackModel = ((_c = (_b = settings.models) === null || _b === void 0 ? void 0 : _b.imageGeneration) === null || _c === void 0 ? void 0 : _c.trim()) ||
567
+ ((_e = (_d = settings.models) === null || _d === void 0 ? void 0 : _d.image) === null || _e === void 0 ? void 0 : _e.trim()) ||
568
+ models_1.MODELS.imageGeneration;
569
+ const timeoutMs = (_f = options.requestTimeoutMs) !== null && _f !== void 0 ? _f : 10 * 60 * 1000;
570
+ const imageAssetStore = (_g = options.imageAssetStore) !== null && _g !== void 0 ? _g : imageAssets_1.defaultServerImageAssetStore;
571
+ const signal = registry.getAbortSignal(jobId);
572
+ if (!signal)
573
+ return null;
574
+ const job = registry.getJob(jobId);
575
+ if (!job || job.kind !== 'images')
576
+ return null;
577
+ const requestedSlotConcurrency = (_h = options.slotConcurrency) !== null && _h !== void 0 ? _h : DEFAULT_SLOT_CONCURRENCY;
578
+ const slotConcurrency = Math.max(1, Math.min(requestedSlotConcurrency, MAX_SLOT_CONCURRENCY, job.slots.length));
579
+ logImageJob(jobId, 'started', {
580
+ workspaceId: job.workspaceId,
581
+ slots: job.slots.length,
582
+ concurrency: slotConcurrency,
583
+ model: fallbackModel,
584
+ });
585
+ let failed = 0;
586
+ const runSlot = (slot) => __awaiter(this, void 0, void 0, function* () {
587
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s;
588
+ if (registry.isCancelled(jobId))
589
+ return;
590
+ try {
591
+ const sourceImageUrl = resolveSourceImageUrl(registry, imageAssetStore, slot);
592
+ let generated;
593
+ if (sourceImageUrl) {
594
+ logImageJob(jobId, 'slot source attached', { index: slot.index });
595
+ generated = { imageUrl: sourceImageUrl, model: slot.model || fallbackModel };
596
+ }
597
+ else {
598
+ const debugModel = slot.model || fallbackModel;
599
+ const debugImageConfig = imageConfigFor(debugModel, (_a = slot.aspectRatio) !== null && _a !== void 0 ? _a : '1:1', slot.imageSize);
600
+ logImageJob(jobId, 'slot generating', {
601
+ index: slot.index,
602
+ approach: slot.approach,
603
+ model: debugModel,
604
+ aspectRatio: (_b = slot.aspectRatio) !== null && _b !== void 0 ? _b : '1:1',
605
+ imageSize: (_c = slot.imageSize) !== null && _c !== void 0 ? _c : null,
606
+ imageConfig: debugImageConfig,
607
+ });
608
+ registry.updateSlot(jobId, slot.index, { status: 'generating' });
609
+ registry.appendEvent(jobId, 'slot.generating', {
610
+ index: slot.index,
611
+ model: debugModel,
612
+ aspectRatio: (_d = slot.aspectRatio) !== null && _d !== void 0 ? _d : '1:1',
613
+ imageSize: (_e = slot.imageSize) !== null && _e !== void 0 ? _e : null,
614
+ imageConfig: debugImageConfig,
615
+ });
616
+ const memoryReferenceImageUrls = [
617
+ ...resolveReferenceImageAssetUrls(imageAssetStore, slot.referenceImageAssetIds),
618
+ ...resolveReferenceImageJobSlotUrls(registry, imageAssetStore, slot.referenceImageJobSlots),
619
+ ];
620
+ generated = yield generateSlotImage({
621
+ apiKey,
622
+ slot,
623
+ fallbackModel,
624
+ memoryReferenceImageUrls,
625
+ signal,
626
+ timeoutMs,
627
+ });
628
+ }
629
+ if (registry.isCancelled(jobId))
630
+ return;
631
+ const generatedDimensions = readImageDimensionsFromDataUrl(generated.imageUrl);
632
+ logImageJob(jobId, 'slot generated', {
633
+ index: slot.index,
634
+ model: generated.model,
635
+ imageBytes: generated.imageUrl.length,
636
+ requestedAspectRatio: (_f = slot.aspectRatio) !== null && _f !== void 0 ? _f : '1:1',
637
+ requestedImageSize: (_g = slot.imageSize) !== null && _g !== void 0 ? _g : null,
638
+ actualWidth: (_h = generatedDimensions === null || generatedDimensions === void 0 ? void 0 : generatedDimensions.width) !== null && _h !== void 0 ? _h : null,
639
+ actualHeight: (_j = generatedDimensions === null || generatedDimensions === void 0 ? void 0 : generatedDimensions.height) !== null && _j !== void 0 ? _j : null,
640
+ actualRatio: generatedDimensions
641
+ ? (generatedDimensions.width / generatedDimensions.height).toFixed(3)
642
+ : null,
643
+ });
644
+ const asset = imageAssetStore.put(generated.imageUrl, {
645
+ jobId,
646
+ slotIndex: slot.index,
647
+ workspaceId: job.workspaceId,
648
+ });
649
+ registry.updateSlot(jobId, slot.index, {
650
+ status: 'generated',
651
+ assetId: asset.id,
652
+ imageDataUrl: generated.imageUrl,
653
+ model: generated.model,
654
+ });
655
+ registry.appendEvent(jobId, 'slot.image.generated', {
656
+ index: slot.index,
657
+ assetId: asset.id,
658
+ imageDataUrl: generated.imageUrl,
659
+ model: generated.model,
660
+ });
661
+ const latestImageJob = registry.getJob(jobId);
662
+ let latestSlot = (latestImageJob === null || latestImageJob === void 0 ? void 0 : latestImageJob.kind) === 'images'
663
+ ? (_k = latestImageJob.slots.find(item => item.index === slot.index)) !== null && _k !== void 0 ? _k : Object.assign(Object.assign({}, slot), { imageDataUrl: generated.imageUrl, model: generated.model })
664
+ : Object.assign(Object.assign({}, slot), { imageDataUrl: generated.imageUrl, model: generated.model });
665
+ if (latestSlot.validationInput) {
666
+ logImageJob(jobId, 'slot validating', { index: slot.index });
667
+ registry.updateSlot(jobId, slot.index, { status: 'validating' });
668
+ registry.appendEvent(jobId, 'slot.validation.started', { index: slot.index });
669
+ const validation = yield validateSlotImage({
670
+ apiKey,
671
+ imageUrl: generated.imageUrl,
672
+ slot: latestSlot,
673
+ fallbackModel: ((_m = (_l = settings.models) === null || _l === void 0 ? void 0 : _l.creativeValidation) === null || _m === void 0 ? void 0 : _m.trim()) ||
674
+ ((_p = (_o = settings.models) === null || _o === void 0 ? void 0 : _o.validation) === null || _p === void 0 ? void 0 : _p.trim()) ||
675
+ models_1.MODELS.creativeValidation,
676
+ signal,
677
+ });
678
+ if (registry.isCancelled(jobId))
679
+ return;
680
+ logImageJob(jobId, 'slot validated', {
681
+ index: slot.index,
682
+ status: validation.status,
683
+ errors: validation.errors.length,
684
+ });
685
+ registry.updateSlot(jobId, slot.index, {
686
+ status: 'validated',
687
+ validation,
688
+ });
689
+ registry.appendEvent(jobId, 'slot.validation.finished', {
690
+ index: slot.index,
691
+ status: validation.status,
692
+ errors: validation.errors,
693
+ });
694
+ if (validation.status !== 'ok') {
695
+ failed += 1;
696
+ return;
697
+ }
698
+ const validatedImageJob = registry.getJob(jobId);
699
+ latestSlot = (validatedImageJob === null || validatedImageJob === void 0 ? void 0 : validatedImageJob.kind) === 'images'
700
+ ? (_q = validatedImageJob.slots.find(item => item.index === slot.index)) !== null && _q !== void 0 ? _q : latestSlot
701
+ : latestSlot;
702
+ }
703
+ if ((_r = latestSlot.upload) === null || _r === void 0 ? void 0 : _r.enabled) {
704
+ logImageJob(jobId, 'slot uploading', {
705
+ index: slot.index,
706
+ folderId: latestSlot.upload.folderId,
707
+ });
708
+ registry.updateSlot(jobId, slot.index, { status: 'uploading' });
709
+ registry.appendEvent(jobId, 'slot.upload.started', { index: slot.index });
710
+ const driveFileId = yield uploadSlotImageToDrive({
711
+ store,
712
+ slot: latestSlot,
713
+ imageUrl: generated.imageUrl,
714
+ });
715
+ if (registry.isCancelled(jobId))
716
+ return;
717
+ logImageJob(jobId, 'slot uploaded', { index: slot.index, driveFileId });
718
+ registry.updateSlot(jobId, slot.index, {
719
+ status: 'uploaded',
720
+ driveFileId,
721
+ });
722
+ registry.appendEvent(jobId, 'slot.upload.finished', { index: slot.index, driveFileId });
723
+ }
724
+ }
725
+ catch (err) {
726
+ if (registry.isCancelled(jobId))
727
+ return;
728
+ failed += 1;
729
+ const message = err instanceof Error ? err.message : String(err);
730
+ logImageJob(jobId, 'slot failed', { index: slot.index, error: message });
731
+ registry.updateSlot(jobId, slot.index, { status: 'failed', error: message });
732
+ registry.appendEvent(jobId, 'job.failed', { index: slot.index, error: message });
733
+ (_s = options.onError) === null || _s === void 0 ? void 0 : _s.call(options, err);
734
+ }
735
+ });
736
+ let nextSlotIndex = 0;
737
+ const workers = Array.from({ length: slotConcurrency }, () => __awaiter(this, void 0, void 0, function* () {
738
+ while (!registry.isCancelled(jobId)) {
739
+ const slot = job.slots[nextSlotIndex];
740
+ nextSlotIndex += 1;
741
+ if (!slot)
742
+ return;
743
+ yield runSlot(slot);
744
+ }
745
+ }));
746
+ yield Promise.all(workers);
747
+ if (registry.isCancelled(jobId)) {
748
+ logImageJob(jobId, 'cancelled');
749
+ return registry.getJob(jobId);
750
+ }
751
+ if (failed > 0) {
752
+ logImageJob(jobId, 'failed', { failed });
753
+ return registry.markJobFailed(jobId, `${failed} image slot(s) failed.`);
754
+ }
755
+ logImageJob(jobId, 'succeeded');
756
+ return registry.markJobSucceeded(jobId);
757
+ });
758
+ }
759
+ function createServerImageJob(payload, registry = jobs_1.defaultServerJobRegistry, store, options = {}) {
760
+ const job = registry.createImageJob(normalizeCreateImageJobPayload(payload));
761
+ logImageJob(job.id, 'created', {
762
+ workspaceId: job.workspaceId,
763
+ slots: job.slots.length,
764
+ start: Boolean(store),
765
+ });
766
+ if (store) {
767
+ queueMicrotask(() => {
768
+ void runServerImageJob(store, registry, job.id, options).catch(err => {
769
+ var _a;
770
+ registry.markJobFailed(job.id, err instanceof Error ? err.message : String(err));
771
+ (_a = options.onError) === null || _a === void 0 ? void 0 : _a.call(options, err);
772
+ });
773
+ });
774
+ }
775
+ return job;
776
+ }