docs-combiner 0.2.2 → 0.4.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.
- package/dist/main.js +1728 -165
- package/dist/preload.js +19 -0
- package/dist/renderer.js +19 -19
- package/dist/renderer.js.LICENSE.txt +4 -0
- package/package.json +4 -2
package/dist/main.js
CHANGED
|
@@ -41,14 +41,39 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
41
41
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
42
42
|
});
|
|
43
43
|
};
|
|
44
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
45
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
46
|
+
};
|
|
44
47
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
48
|
const electron_1 = require("electron");
|
|
49
|
+
const express_1 = __importDefault(require("express"));
|
|
46
50
|
const path = __importStar(require("path"));
|
|
47
51
|
const fs = __importStar(require("fs"));
|
|
48
52
|
const http = __importStar(require("http"));
|
|
49
53
|
const url = __importStar(require("url"));
|
|
50
54
|
let pendingGoogleAuth = null;
|
|
51
55
|
let mainWindow = null;
|
|
56
|
+
let agentApiServer = null;
|
|
57
|
+
const windows = new Set();
|
|
58
|
+
const folderOwners = new Map();
|
|
59
|
+
const windowFolders = new Map();
|
|
60
|
+
const windowFolderPaths = new Map();
|
|
61
|
+
const windowWorkspaces = new Map();
|
|
62
|
+
const windowTitles = new Map();
|
|
63
|
+
const closingWindowIds = new Set();
|
|
64
|
+
const AGENT_API_HOST = '127.0.0.1';
|
|
65
|
+
const DEFAULT_AGENT_API_PORT = 17321;
|
|
66
|
+
const APP_WINDOW_TITLE = 'Комбайнер';
|
|
67
|
+
const AGENT_API_PORT = (() => {
|
|
68
|
+
const rawPort = Number(process.env.DOCS_COMBINER_AGENT_API_PORT);
|
|
69
|
+
return Number.isInteger(rawPort) && rawPort > 0 && rawPort <= 65535
|
|
70
|
+
? rawPort
|
|
71
|
+
: DEFAULT_AGENT_API_PORT;
|
|
72
|
+
})();
|
|
73
|
+
const gotSingleInstanceLock = electron_1.app.requestSingleInstanceLock();
|
|
74
|
+
if (!gotSingleInstanceLock) {
|
|
75
|
+
electron_1.app.quit();
|
|
76
|
+
}
|
|
52
77
|
if (process.env.DOCS_COMBINER_DISABLE_GPU === '1') {
|
|
53
78
|
electron_1.app.disableHardwareAcceleration();
|
|
54
79
|
}
|
|
@@ -56,6 +81,1356 @@ function getInspectableWindow() {
|
|
|
56
81
|
var _a, _b, _c;
|
|
57
82
|
return (_c = (_b = (_a = electron_1.BrowserWindow.getFocusedWindow()) !== null && _a !== void 0 ? _a : mainWindow) !== null && _b !== void 0 ? _b : electron_1.BrowserWindow.getAllWindows()[0]) !== null && _c !== void 0 ? _c : null;
|
|
58
83
|
}
|
|
84
|
+
function createWorkspaceId() {
|
|
85
|
+
return `workspace-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
86
|
+
}
|
|
87
|
+
function getAgentApiBaseUrl() {
|
|
88
|
+
return `http://${AGENT_API_HOST}:${AGENT_API_PORT}`;
|
|
89
|
+
}
|
|
90
|
+
function getWorkspaceWindows() {
|
|
91
|
+
var _a, _b;
|
|
92
|
+
const focusedId = (_b = (_a = electron_1.BrowserWindow.getFocusedWindow()) === null || _a === void 0 ? void 0 : _a.webContents.id) !== null && _b !== void 0 ? _b : null;
|
|
93
|
+
return electron_1.BrowserWindow.getAllWindows()
|
|
94
|
+
.filter(win => !win.isDestroyed() && !win.webContents.isDestroyed() && !closingWindowIds.has(win.webContents.id))
|
|
95
|
+
.map(win => {
|
|
96
|
+
var _a;
|
|
97
|
+
const webContentsId = win.webContents.id;
|
|
98
|
+
const workspaceId = (_a = windowWorkspaces.get(webContentsId)) !== null && _a !== void 0 ? _a : '';
|
|
99
|
+
return {
|
|
100
|
+
workspaceId,
|
|
101
|
+
webContentsId,
|
|
102
|
+
title: windowTitles.get(webContentsId) || 'Новый оффер',
|
|
103
|
+
folderId: windowFolders.get(webContentsId),
|
|
104
|
+
folderPath: windowFolderPaths.get(webContentsId),
|
|
105
|
+
active: webContentsId === focusedId,
|
|
106
|
+
};
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
function getAgentWorkSessions() {
|
|
110
|
+
const baseUrl = getAgentApiBaseUrl();
|
|
111
|
+
return getWorkspaceWindows().map(win => {
|
|
112
|
+
const sessionUrl = `${baseUrl}/api/windows/${encodeURIComponent(win.workspaceId)}`;
|
|
113
|
+
const details = [
|
|
114
|
+
win.title,
|
|
115
|
+
win.folderPath ? `Drive folder: ${win.folderPath}` : 'No Drive folder selected',
|
|
116
|
+
win.active ? 'active session' : 'background session',
|
|
117
|
+
];
|
|
118
|
+
return {
|
|
119
|
+
id: win.workspaceId,
|
|
120
|
+
title: win.title,
|
|
121
|
+
description: details.join('; '),
|
|
122
|
+
folderId: win.folderId,
|
|
123
|
+
folderPath: win.folderPath,
|
|
124
|
+
active: win.active,
|
|
125
|
+
url: sessionUrl,
|
|
126
|
+
endpoints: {
|
|
127
|
+
info: sessionUrl,
|
|
128
|
+
update: sessionUrl,
|
|
129
|
+
setupCampaign: `${sessionUrl}/setup-campaign`,
|
|
130
|
+
geoBlock: `${sessionUrl}/geo-block`,
|
|
131
|
+
creativeSelections: `${sessionUrl}/creative-selections`,
|
|
132
|
+
capabilities: `${sessionUrl}/capabilities`,
|
|
133
|
+
validation: `${sessionUrl}/validation`,
|
|
134
|
+
generated: `${sessionUrl}/generated`,
|
|
135
|
+
logs: `${sessionUrl}/logs`,
|
|
136
|
+
jobs: `${sessionUrl}/jobs`,
|
|
137
|
+
createCampaignGroup: `${sessionUrl}/catalog/groups`,
|
|
138
|
+
createWorkspaceFolder: `${sessionUrl}/catalog/workspaces`,
|
|
139
|
+
generate: `${sessionUrl}/generate`,
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
function getWindowByWorkspaceId(workspaceId) {
|
|
145
|
+
var _a;
|
|
146
|
+
const id = workspaceId.trim();
|
|
147
|
+
return (_a = electron_1.BrowserWindow.getAllWindows().find(win => !win.isDestroyed() &&
|
|
148
|
+
!win.webContents.isDestroyed() &&
|
|
149
|
+
windowWorkspaces.get(win.webContents.id) === id)) !== null && _a !== void 0 ? _a : null;
|
|
150
|
+
}
|
|
151
|
+
function callAgentRendererApi(workspaceId, method, payload) {
|
|
152
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
153
|
+
const win = getWindowByWorkspaceId(workspaceId);
|
|
154
|
+
if (!win) {
|
|
155
|
+
return { ok: false, status: 404, error: 'Session not found.' };
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
const script = `
|
|
159
|
+
(async () => {
|
|
160
|
+
const api = window.__docsCombinerAgentApi;
|
|
161
|
+
if (!api || typeof api[${JSON.stringify(method)}] !== 'function') {
|
|
162
|
+
return { ok: false, error: 'Agent API is not ready in this session.' };
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
const result = await api[${JSON.stringify(method)}](${JSON.stringify(payload !== null && payload !== void 0 ? payload : null)});
|
|
166
|
+
return { ok: true, result };
|
|
167
|
+
} catch (err) {
|
|
168
|
+
return { ok: false, error: err && err.message ? err.message : String(err) };
|
|
169
|
+
}
|
|
170
|
+
})()
|
|
171
|
+
`;
|
|
172
|
+
const response = yield win.webContents.executeJavaScript(script, true);
|
|
173
|
+
if (!(response === null || response === void 0 ? void 0 : response.ok)) {
|
|
174
|
+
return {
|
|
175
|
+
ok: false,
|
|
176
|
+
status: 409,
|
|
177
|
+
error: String((response === null || response === void 0 ? void 0 : response.error) || 'Agent API call failed.'),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
return { ok: true, result: response.result };
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
return {
|
|
184
|
+
ok: false,
|
|
185
|
+
status: 500,
|
|
186
|
+
error: err instanceof Error ? err.message : String(err),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
function callFirstReadyAgentRendererApi(method, payload, preferredSessionId) {
|
|
192
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
193
|
+
const preferredId = String(preferredSessionId !== null && preferredSessionId !== void 0 ? preferredSessionId : '').trim();
|
|
194
|
+
if (preferredId) {
|
|
195
|
+
const preferred = yield callAgentRendererApi(preferredId, method, payload);
|
|
196
|
+
if (preferred.ok)
|
|
197
|
+
return preferred;
|
|
198
|
+
}
|
|
199
|
+
const focused = electron_1.BrowserWindow.getFocusedWindow();
|
|
200
|
+
if (focused && !focused.isDestroyed() && !focused.webContents.isDestroyed()) {
|
|
201
|
+
const focusedId = windowWorkspaces.get(focused.webContents.id);
|
|
202
|
+
if (focusedId) {
|
|
203
|
+
const focusedResponse = yield callAgentRendererApi(focusedId, method, payload);
|
|
204
|
+
if (focusedResponse.ok)
|
|
205
|
+
return focusedResponse;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
for (const win of electron_1.BrowserWindow.getAllWindows()) {
|
|
209
|
+
if (win.isDestroyed() || win.webContents.isDestroyed())
|
|
210
|
+
continue;
|
|
211
|
+
if (win === focused)
|
|
212
|
+
continue;
|
|
213
|
+
const sessionId = windowWorkspaces.get(win.webContents.id);
|
|
214
|
+
if (!sessionId)
|
|
215
|
+
continue;
|
|
216
|
+
const response = yield callAgentRendererApi(sessionId, method, payload);
|
|
217
|
+
if (response.ok)
|
|
218
|
+
return response;
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
ok: false,
|
|
222
|
+
status: 503,
|
|
223
|
+
error: 'No session with Agent API ready.',
|
|
224
|
+
};
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
function waitForAgentSessionReady(sessionId_1) {
|
|
228
|
+
return __awaiter(this, arguments, void 0, function* (sessionId, timeoutMs = 15000) {
|
|
229
|
+
const startedAt = Date.now();
|
|
230
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
231
|
+
const response = yield callAgentRendererApi(sessionId, 'getSessionInfo');
|
|
232
|
+
if (response.ok)
|
|
233
|
+
return response.result;
|
|
234
|
+
yield new Promise(resolve => setTimeout(resolve, 250));
|
|
235
|
+
}
|
|
236
|
+
return null;
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
function getAgentApiSpec(port) {
|
|
240
|
+
return {
|
|
241
|
+
openapi: '3.1.0',
|
|
242
|
+
info: {
|
|
243
|
+
title: 'Docs Combiner Agent API',
|
|
244
|
+
version: electron_1.app.getVersion(),
|
|
245
|
+
description: 'Local-only API for automation agents controlling Docs Combiner.',
|
|
246
|
+
},
|
|
247
|
+
servers: [
|
|
248
|
+
{
|
|
249
|
+
url: `http://${AGENT_API_HOST}:${port}`,
|
|
250
|
+
description: 'Loopback-only Electron main process API',
|
|
251
|
+
},
|
|
252
|
+
],
|
|
253
|
+
paths: {
|
|
254
|
+
'/api/windows': {
|
|
255
|
+
get: {
|
|
256
|
+
operationId: 'listWindows',
|
|
257
|
+
summary: 'List open Docs Combiner work sessions',
|
|
258
|
+
responses: {
|
|
259
|
+
'200': {
|
|
260
|
+
description: 'Current work sessions available to an automation agent',
|
|
261
|
+
content: {
|
|
262
|
+
'application/json': {
|
|
263
|
+
schema: {
|
|
264
|
+
type: 'object',
|
|
265
|
+
required: ['ok', 'windows'],
|
|
266
|
+
properties: {
|
|
267
|
+
ok: { type: 'boolean', const: true },
|
|
268
|
+
windows: {
|
|
269
|
+
type: 'array',
|
|
270
|
+
items: { $ref: '#/components/schemas/AgentWorkSessionInfo' },
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
post: {
|
|
280
|
+
operationId: 'createWindow',
|
|
281
|
+
summary: 'Open a new Docs Combiner work session (Electron window)',
|
|
282
|
+
requestBody: {
|
|
283
|
+
required: false,
|
|
284
|
+
content: {
|
|
285
|
+
'application/json': {
|
|
286
|
+
schema: {
|
|
287
|
+
type: 'object',
|
|
288
|
+
properties: {
|
|
289
|
+
workspaceId: {
|
|
290
|
+
type: 'string',
|
|
291
|
+
description: 'Optional stable session id. Generated if omitted.',
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
responses: {
|
|
299
|
+
'201': {
|
|
300
|
+
description: 'New session created and Agent API is ready in the renderer',
|
|
301
|
+
},
|
|
302
|
+
'202': {
|
|
303
|
+
description: 'New window opened, but renderer Agent API is not ready yet',
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
'/api/catalog': {
|
|
309
|
+
get: {
|
|
310
|
+
operationId: 'getDriveCatalog',
|
|
311
|
+
summary: 'List offers, campaign groups and workspace folders from Google Drive',
|
|
312
|
+
parameters: [
|
|
313
|
+
{
|
|
314
|
+
name: 'sessionId',
|
|
315
|
+
in: 'query',
|
|
316
|
+
required: false,
|
|
317
|
+
schema: { type: 'string' },
|
|
318
|
+
description: 'Prefer data from this session if available.',
|
|
319
|
+
},
|
|
320
|
+
],
|
|
321
|
+
responses: {
|
|
322
|
+
'200': { description: 'Drive catalog snapshot' },
|
|
323
|
+
'503': { description: 'No session with Agent API ready' },
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
'/api/windows/{sessionId}': {
|
|
328
|
+
get: {
|
|
329
|
+
operationId: 'getSessionInfo',
|
|
330
|
+
summary: 'Return all agent-visible information from one work session',
|
|
331
|
+
parameters: [
|
|
332
|
+
{
|
|
333
|
+
name: 'sessionId',
|
|
334
|
+
in: 'path',
|
|
335
|
+
required: true,
|
|
336
|
+
schema: { type: 'string' },
|
|
337
|
+
},
|
|
338
|
+
],
|
|
339
|
+
responses: {
|
|
340
|
+
'200': {
|
|
341
|
+
description: 'Current live session state',
|
|
342
|
+
content: {
|
|
343
|
+
'application/json': {
|
|
344
|
+
schema: {
|
|
345
|
+
type: 'object',
|
|
346
|
+
required: ['ok', 'session'],
|
|
347
|
+
properties: {
|
|
348
|
+
ok: { type: 'boolean', const: true },
|
|
349
|
+
session: { $ref: '#/components/schemas/AgentSessionInfo' },
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
'404': { description: 'Session not found' },
|
|
356
|
+
'409': { description: 'Session renderer is not ready for agent calls' },
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
patch: {
|
|
360
|
+
operationId: 'patchSessionInfo',
|
|
361
|
+
summary: 'Partially update one work session without touching omitted fields',
|
|
362
|
+
parameters: [
|
|
363
|
+
{
|
|
364
|
+
name: 'sessionId',
|
|
365
|
+
in: 'path',
|
|
366
|
+
required: true,
|
|
367
|
+
schema: { type: 'string' },
|
|
368
|
+
},
|
|
369
|
+
],
|
|
370
|
+
requestBody: {
|
|
371
|
+
required: true,
|
|
372
|
+
content: {
|
|
373
|
+
'application/json': {
|
|
374
|
+
schema: { $ref: '#/components/schemas/PatchSessionInfoRequest' },
|
|
375
|
+
examples: {
|
|
376
|
+
productOnly: {
|
|
377
|
+
value: { generation: { product: 'Detoxil Water' } },
|
|
378
|
+
},
|
|
379
|
+
firstGeoOnly: {
|
|
380
|
+
value: { generation: { firstGeo: 'Romania' } },
|
|
381
|
+
},
|
|
382
|
+
geoPriceLinkPartial: {
|
|
383
|
+
value: {
|
|
384
|
+
generation: {
|
|
385
|
+
geoBlock: [
|
|
386
|
+
{ id: 'market-1', link: 'https://example.com/ro/' },
|
|
387
|
+
],
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
},
|
|
391
|
+
creativeSelectionsOnly: {
|
|
392
|
+
value: {
|
|
393
|
+
creativeSelections: {
|
|
394
|
+
selectedTextApproachNumbers: [1, 2, 3],
|
|
395
|
+
},
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
},
|
|
399
|
+
},
|
|
400
|
+
},
|
|
401
|
+
},
|
|
402
|
+
responses: {
|
|
403
|
+
'200': {
|
|
404
|
+
description: 'Session was partially updated',
|
|
405
|
+
content: {
|
|
406
|
+
'application/json': {
|
|
407
|
+
schema: {
|
|
408
|
+
type: 'object',
|
|
409
|
+
required: ['ok', 'session'],
|
|
410
|
+
properties: {
|
|
411
|
+
ok: { type: 'boolean', const: true },
|
|
412
|
+
session: { $ref: '#/components/schemas/AgentSessionInfo' },
|
|
413
|
+
},
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
},
|
|
418
|
+
'400': { description: 'Invalid partial update payload' },
|
|
419
|
+
'404': { description: 'Session not found' },
|
|
420
|
+
'409': { description: 'Session renderer is not ready for agent calls' },
|
|
421
|
+
},
|
|
422
|
+
},
|
|
423
|
+
},
|
|
424
|
+
'/api/windows/{sessionId}/catalog/groups': {
|
|
425
|
+
post: {
|
|
426
|
+
operationId: 'createCampaignGroup',
|
|
427
|
+
summary: 'Create a campaign group folder on Google Drive (same as UI «Создать» for group)',
|
|
428
|
+
parameters: [
|
|
429
|
+
{ name: 'sessionId', in: 'path', required: true, schema: { type: 'string' } },
|
|
430
|
+
],
|
|
431
|
+
requestBody: {
|
|
432
|
+
required: true,
|
|
433
|
+
content: {
|
|
434
|
+
'application/json': {
|
|
435
|
+
schema: {
|
|
436
|
+
type: 'object',
|
|
437
|
+
required: ['name'],
|
|
438
|
+
properties: {
|
|
439
|
+
name: { type: 'string', description: 'Group folder name under campaigns root' },
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
},
|
|
443
|
+
},
|
|
444
|
+
},
|
|
445
|
+
responses: {
|
|
446
|
+
'200': { description: 'Group folder created' },
|
|
447
|
+
'400': { description: 'Invalid payload or group already exists' },
|
|
448
|
+
'404': { description: 'Session not found' },
|
|
449
|
+
'409': { description: 'Session renderer is not ready for agent calls' },
|
|
450
|
+
},
|
|
451
|
+
},
|
|
452
|
+
},
|
|
453
|
+
'/api/windows/{sessionId}/catalog/workspaces': {
|
|
454
|
+
post: {
|
|
455
|
+
operationId: 'createWorkspaceFolder',
|
|
456
|
+
summary: 'Create a workspace folder inside a campaign group on Google Drive',
|
|
457
|
+
parameters: [
|
|
458
|
+
{ name: 'sessionId', in: 'path', required: true, schema: { type: 'string' } },
|
|
459
|
+
],
|
|
460
|
+
requestBody: {
|
|
461
|
+
required: true,
|
|
462
|
+
content: {
|
|
463
|
+
'application/json': {
|
|
464
|
+
schema: {
|
|
465
|
+
type: 'object',
|
|
466
|
+
required: ['groupName', 'name'],
|
|
467
|
+
properties: {
|
|
468
|
+
groupName: { type: 'string' },
|
|
469
|
+
name: { type: 'string', description: 'Workspace folder name' },
|
|
470
|
+
select: {
|
|
471
|
+
type: 'boolean',
|
|
472
|
+
default: true,
|
|
473
|
+
description: 'Select the new workspace in this session after creation',
|
|
474
|
+
},
|
|
475
|
+
},
|
|
476
|
+
},
|
|
477
|
+
},
|
|
478
|
+
},
|
|
479
|
+
},
|
|
480
|
+
responses: {
|
|
481
|
+
'200': { description: 'Workspace folder created' },
|
|
482
|
+
'400': { description: 'Invalid payload or folder already exists' },
|
|
483
|
+
'404': { description: 'Session not found' },
|
|
484
|
+
'409': { description: 'Session renderer is not ready for agent calls' },
|
|
485
|
+
},
|
|
486
|
+
},
|
|
487
|
+
},
|
|
488
|
+
'/api/windows/{sessionId}/generate': {
|
|
489
|
+
post: {
|
|
490
|
+
operationId: 'startGeneration',
|
|
491
|
+
summary: 'Start content, image, catalog or landing generation (same as UI buttons)',
|
|
492
|
+
parameters: [
|
|
493
|
+
{ name: 'sessionId', in: 'path', required: true, schema: { type: 'string' } },
|
|
494
|
+
],
|
|
495
|
+
requestBody: {
|
|
496
|
+
required: false,
|
|
497
|
+
content: {
|
|
498
|
+
'application/json': {
|
|
499
|
+
schema: {
|
|
500
|
+
type: 'object',
|
|
501
|
+
properties: {
|
|
502
|
+
targets: {
|
|
503
|
+
type: 'array',
|
|
504
|
+
items: {
|
|
505
|
+
type: 'string',
|
|
506
|
+
enum: ['content', 'images', 'catalog', 'landing', 'all'],
|
|
507
|
+
},
|
|
508
|
+
description: 'Sequential targets. Default: ["content"]. "all" expands to content + images.',
|
|
509
|
+
},
|
|
510
|
+
marketIds: {
|
|
511
|
+
type: 'array',
|
|
512
|
+
items: { type: 'string' },
|
|
513
|
+
description: 'Optional market ids for landing target only',
|
|
514
|
+
},
|
|
515
|
+
},
|
|
516
|
+
},
|
|
517
|
+
examples: {
|
|
518
|
+
contentAndImages: {
|
|
519
|
+
value: { targets: ['content', 'images'] },
|
|
520
|
+
},
|
|
521
|
+
fullPipeline: {
|
|
522
|
+
value: { targets: ['content', 'images', 'catalog', 'landing'] },
|
|
523
|
+
},
|
|
524
|
+
},
|
|
525
|
+
},
|
|
526
|
+
},
|
|
527
|
+
},
|
|
528
|
+
responses: {
|
|
529
|
+
'200': { description: 'Generation started or completed for requested targets' },
|
|
530
|
+
'400': { description: 'Validation failed or unknown target' },
|
|
531
|
+
'404': { description: 'Session not found' },
|
|
532
|
+
'409': { description: 'Session renderer is not ready or job already running' },
|
|
533
|
+
},
|
|
534
|
+
},
|
|
535
|
+
},
|
|
536
|
+
'/api/windows/{sessionId}/setup-campaign': {
|
|
537
|
+
post: {
|
|
538
|
+
operationId: 'setupCampaignWorkspace',
|
|
539
|
+
summary: 'Select campaign group, workspace folder, offer binding and optional generation fields',
|
|
540
|
+
parameters: [
|
|
541
|
+
{
|
|
542
|
+
name: 'sessionId',
|
|
543
|
+
in: 'path',
|
|
544
|
+
required: true,
|
|
545
|
+
schema: { type: 'string' },
|
|
546
|
+
},
|
|
547
|
+
],
|
|
548
|
+
requestBody: {
|
|
549
|
+
required: true,
|
|
550
|
+
content: {
|
|
551
|
+
'application/json': {
|
|
552
|
+
schema: { $ref: '#/components/schemas/SetupCampaignWorkspaceRequest' },
|
|
553
|
+
},
|
|
554
|
+
},
|
|
555
|
+
},
|
|
556
|
+
responses: {
|
|
557
|
+
'200': { description: 'Campaign workspace configured' },
|
|
558
|
+
'400': { description: 'Invalid payload or workspace not found' },
|
|
559
|
+
'404': { description: 'Session not found' },
|
|
560
|
+
'409': { description: 'Session renderer is not ready for agent calls' },
|
|
561
|
+
},
|
|
562
|
+
},
|
|
563
|
+
},
|
|
564
|
+
'/api/windows/{sessionId}/geo-block': {
|
|
565
|
+
post: {
|
|
566
|
+
operationId: 'fillGeoBlock',
|
|
567
|
+
summary: 'Update the geo + price + link block in one work session',
|
|
568
|
+
description: 'A single object partially updates one row and preserves omitted fields. A markets array sets the exact row list, removing rows not present in the array; omitted fields inside each row are preserved by id/index when possible.',
|
|
569
|
+
parameters: [
|
|
570
|
+
{
|
|
571
|
+
name: 'sessionId',
|
|
572
|
+
in: 'path',
|
|
573
|
+
required: true,
|
|
574
|
+
schema: { type: 'string' },
|
|
575
|
+
},
|
|
576
|
+
],
|
|
577
|
+
requestBody: {
|
|
578
|
+
required: true,
|
|
579
|
+
content: {
|
|
580
|
+
'application/json': {
|
|
581
|
+
schema: { $ref: '#/components/schemas/FillGeoBlockRequest' },
|
|
582
|
+
examples: {
|
|
583
|
+
singleMarket: {
|
|
584
|
+
value: {
|
|
585
|
+
geo: 'Romania',
|
|
586
|
+
priceWithCurrency: '149 RON',
|
|
587
|
+
link: 'https://example.com/product/',
|
|
588
|
+
},
|
|
589
|
+
},
|
|
590
|
+
multipleMarkets: {
|
|
591
|
+
value: {
|
|
592
|
+
markets: [
|
|
593
|
+
{
|
|
594
|
+
geo: 'Romania',
|
|
595
|
+
priceWithCurrency: '149 RON',
|
|
596
|
+
link: 'https://example.com/ro/',
|
|
597
|
+
},
|
|
598
|
+
{
|
|
599
|
+
geo: 'Hungary',
|
|
600
|
+
priceWithCurrency: '11400 HUF',
|
|
601
|
+
link: 'https://example.com/hu/',
|
|
602
|
+
},
|
|
603
|
+
],
|
|
604
|
+
},
|
|
605
|
+
},
|
|
606
|
+
},
|
|
607
|
+
},
|
|
608
|
+
},
|
|
609
|
+
},
|
|
610
|
+
responses: {
|
|
611
|
+
'200': {
|
|
612
|
+
description: 'Geo block was replaced',
|
|
613
|
+
content: {
|
|
614
|
+
'application/json': {
|
|
615
|
+
schema: {
|
|
616
|
+
type: 'object',
|
|
617
|
+
required: ['ok', 'markets'],
|
|
618
|
+
properties: {
|
|
619
|
+
ok: { type: 'boolean', const: true },
|
|
620
|
+
markets: {
|
|
621
|
+
type: 'array',
|
|
622
|
+
items: { $ref: '#/components/schemas/GeoBlockMarket' },
|
|
623
|
+
},
|
|
624
|
+
},
|
|
625
|
+
},
|
|
626
|
+
},
|
|
627
|
+
},
|
|
628
|
+
},
|
|
629
|
+
'400': { description: 'Invalid geo block payload' },
|
|
630
|
+
'404': { description: 'Session not found' },
|
|
631
|
+
'409': { description: 'Session renderer is not ready for agent calls' },
|
|
632
|
+
},
|
|
633
|
+
},
|
|
634
|
+
},
|
|
635
|
+
'/api/windows/{sessionId}/creative-selections': {
|
|
636
|
+
get: {
|
|
637
|
+
operationId: 'getCreativeSelections',
|
|
638
|
+
summary: 'Return selected text and image creative approaches for one work session',
|
|
639
|
+
parameters: [
|
|
640
|
+
{
|
|
641
|
+
name: 'sessionId',
|
|
642
|
+
in: 'path',
|
|
643
|
+
required: true,
|
|
644
|
+
schema: { type: 'string' },
|
|
645
|
+
},
|
|
646
|
+
],
|
|
647
|
+
responses: {
|
|
648
|
+
'200': {
|
|
649
|
+
description: 'Current creative approach selections',
|
|
650
|
+
content: {
|
|
651
|
+
'application/json': {
|
|
652
|
+
schema: {
|
|
653
|
+
type: 'object',
|
|
654
|
+
required: ['ok', 'selections'],
|
|
655
|
+
properties: {
|
|
656
|
+
ok: { type: 'boolean', const: true },
|
|
657
|
+
selections: { $ref: '#/components/schemas/CreativeSelections' },
|
|
658
|
+
},
|
|
659
|
+
},
|
|
660
|
+
},
|
|
661
|
+
},
|
|
662
|
+
},
|
|
663
|
+
'404': { description: 'Session not found' },
|
|
664
|
+
'409': { description: 'Session renderer is not ready for agent calls' },
|
|
665
|
+
},
|
|
666
|
+
},
|
|
667
|
+
post: {
|
|
668
|
+
operationId: 'setCreativeSelections',
|
|
669
|
+
summary: 'Set selected text and image creative approaches for one work session',
|
|
670
|
+
parameters: [
|
|
671
|
+
{
|
|
672
|
+
name: 'sessionId',
|
|
673
|
+
in: 'path',
|
|
674
|
+
required: true,
|
|
675
|
+
schema: { type: 'string' },
|
|
676
|
+
},
|
|
677
|
+
],
|
|
678
|
+
requestBody: {
|
|
679
|
+
required: true,
|
|
680
|
+
content: {
|
|
681
|
+
'application/json': {
|
|
682
|
+
schema: { $ref: '#/components/schemas/SetCreativeSelectionsRequest' },
|
|
683
|
+
examples: {
|
|
684
|
+
uiNumbers: {
|
|
685
|
+
value: {
|
|
686
|
+
selectedTextApproachNumbers: [1, 2, 3],
|
|
687
|
+
imageApproachCounts: [1, 1, 0, 0, 2, 0, 0, 0, 0, 0],
|
|
688
|
+
},
|
|
689
|
+
},
|
|
690
|
+
imageNumbers: {
|
|
691
|
+
value: {
|
|
692
|
+
selectedTextApproachIndices: [0, 1, 2],
|
|
693
|
+
selectedImageApproachNumbers: [1, 5],
|
|
694
|
+
},
|
|
695
|
+
},
|
|
696
|
+
},
|
|
697
|
+
},
|
|
698
|
+
},
|
|
699
|
+
},
|
|
700
|
+
responses: {
|
|
701
|
+
'200': {
|
|
702
|
+
description: 'Creative approach selections were updated',
|
|
703
|
+
content: {
|
|
704
|
+
'application/json': {
|
|
705
|
+
schema: {
|
|
706
|
+
type: 'object',
|
|
707
|
+
required: ['ok', 'selections'],
|
|
708
|
+
properties: {
|
|
709
|
+
ok: { type: 'boolean', const: true },
|
|
710
|
+
selections: { $ref: '#/components/schemas/CreativeSelections' },
|
|
711
|
+
},
|
|
712
|
+
},
|
|
713
|
+
},
|
|
714
|
+
},
|
|
715
|
+
},
|
|
716
|
+
'400': { description: 'Invalid creative selections payload' },
|
|
717
|
+
'404': { description: 'Session not found' },
|
|
718
|
+
'409': { description: 'Session renderer is not ready for agent calls' },
|
|
719
|
+
},
|
|
720
|
+
},
|
|
721
|
+
},
|
|
722
|
+
'/api/windows/{sessionId}/capabilities': {
|
|
723
|
+
get: {
|
|
724
|
+
operationId: 'getCapabilities',
|
|
725
|
+
summary: 'Return what the agent is allowed to read or change in this session',
|
|
726
|
+
responses: { '200': { description: 'Session capabilities' } },
|
|
727
|
+
},
|
|
728
|
+
},
|
|
729
|
+
'/api/windows/{sessionId}/validation': {
|
|
730
|
+
get: {
|
|
731
|
+
operationId: 'getValidation',
|
|
732
|
+
summary: 'Return readiness checks and blocking issues for this session',
|
|
733
|
+
responses: { '200': { description: 'Validation checklist' } },
|
|
734
|
+
},
|
|
735
|
+
},
|
|
736
|
+
'/api/windows/{sessionId}/jobs': {
|
|
737
|
+
get: {
|
|
738
|
+
operationId: 'listJobs',
|
|
739
|
+
summary: 'Return observable UI job statuses for this session',
|
|
740
|
+
responses: { '200': { description: 'Known job statuses' } },
|
|
741
|
+
},
|
|
742
|
+
},
|
|
743
|
+
'/api/windows/{sessionId}/jobs/{jobId}': {
|
|
744
|
+
get: {
|
|
745
|
+
operationId: 'getJobStatus',
|
|
746
|
+
summary: 'Return one observable UI job status',
|
|
747
|
+
parameters: [
|
|
748
|
+
{ name: 'sessionId', in: 'path', required: true, schema: { type: 'string' } },
|
|
749
|
+
{ name: 'jobId', in: 'path', required: true, schema: { type: 'string' } },
|
|
750
|
+
],
|
|
751
|
+
responses: { '200': { description: 'Job status' }, '404': { description: 'Unknown job id' } },
|
|
752
|
+
},
|
|
753
|
+
},
|
|
754
|
+
'/api/windows/{sessionId}/generated': {
|
|
755
|
+
get: {
|
|
756
|
+
operationId: 'getGenerated',
|
|
757
|
+
summary: 'Return generated titles, texts, images, catalog rows and landing artifacts',
|
|
758
|
+
responses: { '200': { description: 'Generated session artifacts' } },
|
|
759
|
+
},
|
|
760
|
+
patch: {
|
|
761
|
+
operationId: 'patchGenerated',
|
|
762
|
+
summary: 'Partially edit generated titles, texts or image metadata without starting generation',
|
|
763
|
+
requestBody: {
|
|
764
|
+
required: true,
|
|
765
|
+
content: {
|
|
766
|
+
'application/json': {
|
|
767
|
+
schema: { $ref: '#/components/schemas/PatchGeneratedRequest' },
|
|
768
|
+
examples: {
|
|
769
|
+
titleText: {
|
|
770
|
+
value: {
|
|
771
|
+
titles: [{ index: 1, title: 'New title' }],
|
|
772
|
+
texts: [{ index: 1, text: 'New text' }],
|
|
773
|
+
},
|
|
774
|
+
},
|
|
775
|
+
imageMetadata: {
|
|
776
|
+
value: {
|
|
777
|
+
images: [{ index: 1, failed: false, errorMessage: '' }],
|
|
778
|
+
},
|
|
779
|
+
},
|
|
780
|
+
},
|
|
781
|
+
},
|
|
782
|
+
},
|
|
783
|
+
},
|
|
784
|
+
responses: { '200': { description: 'Generated artifacts were updated' }, '400': { description: 'Invalid patch' } },
|
|
785
|
+
},
|
|
786
|
+
},
|
|
787
|
+
'/api/windows/{sessionId}/logs': {
|
|
788
|
+
get: {
|
|
789
|
+
operationId: 'getLogs',
|
|
790
|
+
summary: 'Return recent content, image, landing and catalog replacement logs',
|
|
791
|
+
responses: { '200': { description: 'Session logs' } },
|
|
792
|
+
},
|
|
793
|
+
},
|
|
794
|
+
'/api/spec': {
|
|
795
|
+
get: {
|
|
796
|
+
operationId: 'getAgentApiSpec',
|
|
797
|
+
summary: 'Return the machine-readable Agent API description',
|
|
798
|
+
responses: {
|
|
799
|
+
'200': {
|
|
800
|
+
description: 'OpenAPI-like API description for automation agents',
|
|
801
|
+
},
|
|
802
|
+
},
|
|
803
|
+
},
|
|
804
|
+
},
|
|
805
|
+
},
|
|
806
|
+
components: {
|
|
807
|
+
schemas: {
|
|
808
|
+
AgentWorkSessionInfo: {
|
|
809
|
+
type: 'object',
|
|
810
|
+
required: ['id', 'title', 'description', 'active', 'url', 'endpoints'],
|
|
811
|
+
properties: {
|
|
812
|
+
id: {
|
|
813
|
+
type: 'string',
|
|
814
|
+
description: 'Stable workspace/session id to use in future agent operations.',
|
|
815
|
+
},
|
|
816
|
+
title: { type: 'string' },
|
|
817
|
+
description: {
|
|
818
|
+
type: 'string',
|
|
819
|
+
description: 'Short human-readable summary of what is open in this session.',
|
|
820
|
+
},
|
|
821
|
+
folderId: {
|
|
822
|
+
type: 'string',
|
|
823
|
+
description: 'Current Google Drive folder id, when selected.',
|
|
824
|
+
},
|
|
825
|
+
folderPath: {
|
|
826
|
+
type: 'string',
|
|
827
|
+
description: 'Two-level display path for the current Drive folder, for example OFFERS/OfferName.',
|
|
828
|
+
},
|
|
829
|
+
active: { type: 'boolean' },
|
|
830
|
+
url: {
|
|
831
|
+
type: 'string',
|
|
832
|
+
description: 'Full local URL for reading this session state.',
|
|
833
|
+
},
|
|
834
|
+
endpoints: {
|
|
835
|
+
type: 'object',
|
|
836
|
+
required: ['info', 'update', 'geoBlock', 'creativeSelections', 'capabilities', 'validation', 'generated', 'logs', 'jobs'],
|
|
837
|
+
properties: {
|
|
838
|
+
info: { type: 'string' },
|
|
839
|
+
update: { type: 'string' },
|
|
840
|
+
geoBlock: { type: 'string' },
|
|
841
|
+
creativeSelections: { type: 'string' },
|
|
842
|
+
capabilities: { type: 'string' },
|
|
843
|
+
validation: { type: 'string' },
|
|
844
|
+
generated: { type: 'string' },
|
|
845
|
+
logs: { type: 'string' },
|
|
846
|
+
jobs: { type: 'string' },
|
|
847
|
+
},
|
|
848
|
+
},
|
|
849
|
+
},
|
|
850
|
+
},
|
|
851
|
+
GeoBlockMarket: {
|
|
852
|
+
type: 'object',
|
|
853
|
+
properties: {
|
|
854
|
+
id: { type: 'string' },
|
|
855
|
+
geo: { type: 'string' },
|
|
856
|
+
priceWithCurrency: { type: 'string' },
|
|
857
|
+
link: { type: 'string' },
|
|
858
|
+
},
|
|
859
|
+
},
|
|
860
|
+
FillGeoBlockRequest: {
|
|
861
|
+
oneOf: [
|
|
862
|
+
{ $ref: '#/components/schemas/GeoBlockMarket' },
|
|
863
|
+
{
|
|
864
|
+
type: 'object',
|
|
865
|
+
required: ['markets'],
|
|
866
|
+
properties: {
|
|
867
|
+
markets: {
|
|
868
|
+
type: 'array',
|
|
869
|
+
minItems: 1,
|
|
870
|
+
items: { $ref: '#/components/schemas/GeoBlockMarket' },
|
|
871
|
+
},
|
|
872
|
+
},
|
|
873
|
+
},
|
|
874
|
+
],
|
|
875
|
+
},
|
|
876
|
+
PatchSessionInfoRequest: {
|
|
877
|
+
type: 'object',
|
|
878
|
+
description: 'Partial session update. Omitted fields are left unchanged.',
|
|
879
|
+
properties: {
|
|
880
|
+
generateProduct: { type: 'string' },
|
|
881
|
+
generateAdditionalInfo: { type: 'string' },
|
|
882
|
+
generateGeo: { type: 'string' },
|
|
883
|
+
generatePriceWithCurrency: { type: 'string' },
|
|
884
|
+
link: { type: 'string' },
|
|
885
|
+
generationMarkets: {
|
|
886
|
+
type: 'array',
|
|
887
|
+
items: { $ref: '#/components/schemas/GeoBlockMarket' },
|
|
888
|
+
},
|
|
889
|
+
markets: {
|
|
890
|
+
type: 'array',
|
|
891
|
+
items: { $ref: '#/components/schemas/GeoBlockMarket' },
|
|
892
|
+
},
|
|
893
|
+
geoBlock: {
|
|
894
|
+
type: 'array',
|
|
895
|
+
items: { $ref: '#/components/schemas/GeoBlockMarket' },
|
|
896
|
+
},
|
|
897
|
+
generation: {
|
|
898
|
+
type: 'object',
|
|
899
|
+
properties: {
|
|
900
|
+
product: { type: 'string' },
|
|
901
|
+
additionalInfo: { type: 'string' },
|
|
902
|
+
firstGeo: { type: 'string' },
|
|
903
|
+
firstPriceWithCurrency: { type: 'string' },
|
|
904
|
+
firstLink: { type: 'string' },
|
|
905
|
+
geoBlock: {
|
|
906
|
+
type: 'array',
|
|
907
|
+
items: { $ref: '#/components/schemas/GeoBlockMarket' },
|
|
908
|
+
},
|
|
909
|
+
},
|
|
910
|
+
},
|
|
911
|
+
catalogUrlSettings: { type: 'object' },
|
|
912
|
+
creativeSelections: { $ref: '#/components/schemas/SetCreativeSelectionsRequest' },
|
|
913
|
+
promptSettings: { type: 'object' },
|
|
914
|
+
drive: {
|
|
915
|
+
type: 'object',
|
|
916
|
+
description: 'Select campaign group, workspace folder and offer binding before generation.',
|
|
917
|
+
properties: {
|
|
918
|
+
groupName: { type: 'string' },
|
|
919
|
+
workspaceFolderId: { type: 'string' },
|
|
920
|
+
workspaceName: { type: 'string' },
|
|
921
|
+
offer: { type: 'string' },
|
|
922
|
+
},
|
|
923
|
+
},
|
|
924
|
+
},
|
|
925
|
+
},
|
|
926
|
+
SetupCampaignWorkspaceRequest: {
|
|
927
|
+
type: 'object',
|
|
928
|
+
required: ['groupName'],
|
|
929
|
+
properties: {
|
|
930
|
+
groupName: { type: 'string' },
|
|
931
|
+
workspaceFolderId: { type: 'string' },
|
|
932
|
+
workspaceName: { type: 'string' },
|
|
933
|
+
offer: { type: 'string' },
|
|
934
|
+
generation: { type: 'object' },
|
|
935
|
+
creativeSelections: { $ref: '#/components/schemas/SetCreativeSelectionsRequest' },
|
|
936
|
+
},
|
|
937
|
+
},
|
|
938
|
+
DriveCatalog: {
|
|
939
|
+
type: 'object',
|
|
940
|
+
description: 'Offers root, campaigns tree and current session Drive bindings.',
|
|
941
|
+
},
|
|
942
|
+
CreativeSelections: {
|
|
943
|
+
type: 'object',
|
|
944
|
+
required: ['textApproaches', 'imageApproaches'],
|
|
945
|
+
properties: {
|
|
946
|
+
textApproaches: {
|
|
947
|
+
type: 'object',
|
|
948
|
+
required: ['selectedIndices', 'selectedNumbers'],
|
|
949
|
+
properties: {
|
|
950
|
+
selectedIndices: {
|
|
951
|
+
type: 'array',
|
|
952
|
+
description: '0-based selected text approach indices.',
|
|
953
|
+
items: { type: 'number' },
|
|
954
|
+
},
|
|
955
|
+
selectedNumbers: {
|
|
956
|
+
type: 'array',
|
|
957
|
+
description: '1-based selected text approach numbers as shown in the UI.',
|
|
958
|
+
items: { type: 'number' },
|
|
959
|
+
},
|
|
960
|
+
},
|
|
961
|
+
},
|
|
962
|
+
imageApproaches: {
|
|
963
|
+
type: 'object',
|
|
964
|
+
required: ['counts', 'selectedIndices', 'selectedNumbers', 'maxPerApproach'],
|
|
965
|
+
properties: {
|
|
966
|
+
counts: {
|
|
967
|
+
type: 'array',
|
|
968
|
+
description: 'Image count per creative approach, 10 items, values from 0 to maxPerApproach.',
|
|
969
|
+
items: { type: 'number' },
|
|
970
|
+
},
|
|
971
|
+
selectedIndices: {
|
|
972
|
+
type: 'array',
|
|
973
|
+
description: '0-based image approach indices where count is greater than 0.',
|
|
974
|
+
items: { type: 'number' },
|
|
975
|
+
},
|
|
976
|
+
selectedNumbers: {
|
|
977
|
+
type: 'array',
|
|
978
|
+
description: '1-based image approach numbers where count is greater than 0.',
|
|
979
|
+
items: { type: 'number' },
|
|
980
|
+
},
|
|
981
|
+
maxPerApproach: { type: 'number' },
|
|
982
|
+
},
|
|
983
|
+
},
|
|
984
|
+
},
|
|
985
|
+
},
|
|
986
|
+
SetCreativeSelectionsRequest: {
|
|
987
|
+
type: 'object',
|
|
988
|
+
properties: {
|
|
989
|
+
selectedTextApproachIndices: {
|
|
990
|
+
type: 'array',
|
|
991
|
+
description: '0-based text approach indices. At least two unique approaches are required if provided.',
|
|
992
|
+
items: { type: 'number' },
|
|
993
|
+
},
|
|
994
|
+
selectedTextApproachNumbers: {
|
|
995
|
+
type: 'array',
|
|
996
|
+
description: '1-based text approach numbers as shown in the UI. At least two unique approaches are required if provided.',
|
|
997
|
+
items: { type: 'number' },
|
|
998
|
+
},
|
|
999
|
+
imageApproachCounts: {
|
|
1000
|
+
type: 'array',
|
|
1001
|
+
description: 'Image count per creative approach, 10 items, values from 0 to maxPerApproach.',
|
|
1002
|
+
items: { type: 'number' },
|
|
1003
|
+
},
|
|
1004
|
+
selectedImageApproachIndices: {
|
|
1005
|
+
type: 'array',
|
|
1006
|
+
description: '0-based image approach indices. Each selected approach gets count=1.',
|
|
1007
|
+
items: { type: 'number' },
|
|
1008
|
+
},
|
|
1009
|
+
selectedImageApproachNumbers: {
|
|
1010
|
+
type: 'array',
|
|
1011
|
+
description: '1-based image approach numbers as shown in the UI. Each selected approach gets count=1.',
|
|
1012
|
+
items: { type: 'number' },
|
|
1013
|
+
},
|
|
1014
|
+
},
|
|
1015
|
+
},
|
|
1016
|
+
PatchGeneratedRequest: {
|
|
1017
|
+
type: 'object',
|
|
1018
|
+
description: 'Partial edit for generated artifacts. Omitted fields are unchanged.',
|
|
1019
|
+
properties: {
|
|
1020
|
+
titles: {
|
|
1021
|
+
type: 'array',
|
|
1022
|
+
items: {
|
|
1023
|
+
type: 'object',
|
|
1024
|
+
required: ['index'],
|
|
1025
|
+
properties: {
|
|
1026
|
+
index: { type: 'number', description: '1-based title row index.' },
|
|
1027
|
+
title: { type: 'string' },
|
|
1028
|
+
failed: { type: 'boolean' },
|
|
1029
|
+
errorMessage: { type: 'string' },
|
|
1030
|
+
},
|
|
1031
|
+
},
|
|
1032
|
+
},
|
|
1033
|
+
texts: {
|
|
1034
|
+
type: 'array',
|
|
1035
|
+
items: {
|
|
1036
|
+
type: 'object',
|
|
1037
|
+
required: ['index'],
|
|
1038
|
+
properties: {
|
|
1039
|
+
index: { type: 'number', description: '1-based text row index.' },
|
|
1040
|
+
text: { type: 'string' },
|
|
1041
|
+
failed: { type: 'boolean' },
|
|
1042
|
+
errorMessage: { type: 'string' },
|
|
1043
|
+
},
|
|
1044
|
+
},
|
|
1045
|
+
},
|
|
1046
|
+
images: {
|
|
1047
|
+
type: 'array',
|
|
1048
|
+
items: {
|
|
1049
|
+
type: 'object',
|
|
1050
|
+
required: ['index'],
|
|
1051
|
+
properties: {
|
|
1052
|
+
index: { type: 'number', description: '1-based image slot index.' },
|
|
1053
|
+
imageUrl: { type: 'string' },
|
|
1054
|
+
approach: { type: 'string' },
|
|
1055
|
+
geo: { type: 'string' },
|
|
1056
|
+
priceWithCurrency: { type: 'string' },
|
|
1057
|
+
aspectRatio: { type: 'string' },
|
|
1058
|
+
uploaded: { type: 'boolean' },
|
|
1059
|
+
failed: { type: 'boolean' },
|
|
1060
|
+
errorMessage: { type: 'string' },
|
|
1061
|
+
checkStatus: { type: 'string' },
|
|
1062
|
+
checkResult: { type: 'string' },
|
|
1063
|
+
checkErrors: { type: 'array', items: { type: 'string' } },
|
|
1064
|
+
},
|
|
1065
|
+
},
|
|
1066
|
+
},
|
|
1067
|
+
},
|
|
1068
|
+
},
|
|
1069
|
+
AgentSessionInfo: {
|
|
1070
|
+
type: 'object',
|
|
1071
|
+
description: 'Live renderer state for one Docs Combiner work session.',
|
|
1072
|
+
required: ['id', 'title', 'updatedAt', 'generation', 'snapshot'],
|
|
1073
|
+
properties: {
|
|
1074
|
+
id: { type: 'string' },
|
|
1075
|
+
title: { type: 'string' },
|
|
1076
|
+
updatedAt: { type: 'string', format: 'date-time' },
|
|
1077
|
+
drive: { type: 'object' },
|
|
1078
|
+
generation: {
|
|
1079
|
+
type: 'object',
|
|
1080
|
+
properties: {
|
|
1081
|
+
product: { type: 'string' },
|
|
1082
|
+
additionalInfo: { type: 'string' },
|
|
1083
|
+
geoBlock: {
|
|
1084
|
+
type: 'array',
|
|
1085
|
+
items: { $ref: '#/components/schemas/GeoBlockMarket' },
|
|
1086
|
+
},
|
|
1087
|
+
firstGeo: { type: 'string' },
|
|
1088
|
+
firstPriceWithCurrency: { type: 'string' },
|
|
1089
|
+
firstLink: { type: 'string' },
|
|
1090
|
+
brand: { type: 'string' },
|
|
1091
|
+
},
|
|
1092
|
+
},
|
|
1093
|
+
catalogUrlSettings: { type: 'object' },
|
|
1094
|
+
promptSettings: { type: 'object' },
|
|
1095
|
+
snapshot: {
|
|
1096
|
+
type: 'object',
|
|
1097
|
+
description: 'Serializable workspace snapshot used by the app autosave.',
|
|
1098
|
+
},
|
|
1099
|
+
},
|
|
1100
|
+
},
|
|
1101
|
+
},
|
|
1102
|
+
},
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
function startAgentApiServer() {
|
|
1106
|
+
if (agentApiServer)
|
|
1107
|
+
return;
|
|
1108
|
+
const api = (0, express_1.default)();
|
|
1109
|
+
api.disable('x-powered-by');
|
|
1110
|
+
api.use(express_1.default.json({ limit: '1mb' }));
|
|
1111
|
+
api.get('/api/windows', (_req, res) => {
|
|
1112
|
+
res.json({
|
|
1113
|
+
ok: true,
|
|
1114
|
+
windows: getAgentWorkSessions(),
|
|
1115
|
+
});
|
|
1116
|
+
});
|
|
1117
|
+
api.post('/api/windows', (req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
1118
|
+
var _a, _b, _c;
|
|
1119
|
+
const requestedId = String((_b = (_a = req.body) === null || _a === void 0 ? void 0 : _a.workspaceId) !== null && _b !== void 0 ? _b : '').trim();
|
|
1120
|
+
const workspaceId = requestedId || createWorkspaceId();
|
|
1121
|
+
createWindow(workspaceId);
|
|
1122
|
+
broadcastWorkspaceWindowsUpdated();
|
|
1123
|
+
const session = yield waitForAgentSessionReady(workspaceId);
|
|
1124
|
+
const baseUrl = getAgentApiBaseUrl();
|
|
1125
|
+
const sessionUrl = `${baseUrl}/api/windows/${encodeURIComponent(workspaceId)}`;
|
|
1126
|
+
if (session) {
|
|
1127
|
+
res.status(201).json({
|
|
1128
|
+
ok: true,
|
|
1129
|
+
ready: true,
|
|
1130
|
+
session,
|
|
1131
|
+
url: sessionUrl,
|
|
1132
|
+
endpoints: (_c = getAgentWorkSessions().find(item => item.id === workspaceId)) === null || _c === void 0 ? void 0 : _c.endpoints,
|
|
1133
|
+
});
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
res.status(202).json({
|
|
1137
|
+
ok: true,
|
|
1138
|
+
ready: false,
|
|
1139
|
+
session: { id: workspaceId, title: 'Новый оффер' },
|
|
1140
|
+
url: sessionUrl,
|
|
1141
|
+
message: 'Window opened, but renderer Agent API is not ready yet. Retry GET /api/windows/{sessionId}.',
|
|
1142
|
+
});
|
|
1143
|
+
}));
|
|
1144
|
+
api.get('/api/catalog', (req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
1145
|
+
var _a;
|
|
1146
|
+
const preferredSessionId = String((_a = req.query.sessionId) !== null && _a !== void 0 ? _a : '').trim();
|
|
1147
|
+
const response = yield callFirstReadyAgentRendererApi('getDriveCatalog', null, preferredSessionId || undefined);
|
|
1148
|
+
if (!response.ok) {
|
|
1149
|
+
res.status(response.status).json({ ok: false, error: response.error });
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
res.json({ ok: true, catalog: response.result });
|
|
1153
|
+
}));
|
|
1154
|
+
api.get('/api/windows/:sessionId', (req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
1155
|
+
var _a;
|
|
1156
|
+
const sessionId = String((_a = req.params.sessionId) !== null && _a !== void 0 ? _a : '').trim();
|
|
1157
|
+
if (!sessionId) {
|
|
1158
|
+
res.status(400).json({ ok: false, error: 'Session id is required.' });
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
const response = yield callAgentRendererApi(sessionId, 'getSessionInfo');
|
|
1162
|
+
if (!response.ok) {
|
|
1163
|
+
res.status(response.status).json({ ok: false, error: response.error });
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
res.json({ ok: true, session: response.result });
|
|
1167
|
+
}));
|
|
1168
|
+
api.patch('/api/windows/:sessionId', (req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
1169
|
+
var _a, _b;
|
|
1170
|
+
const sessionId = String((_a = req.params.sessionId) !== null && _a !== void 0 ? _a : '').trim();
|
|
1171
|
+
if (!sessionId) {
|
|
1172
|
+
res.status(400).json({ ok: false, error: 'Session id is required.' });
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
const response = yield callAgentRendererApi(sessionId, 'patchSessionInfo', req.body);
|
|
1176
|
+
if (!response.ok) {
|
|
1177
|
+
res.status(response.status).json({ ok: false, error: response.error });
|
|
1178
|
+
return;
|
|
1179
|
+
}
|
|
1180
|
+
if (((_b = response.result) === null || _b === void 0 ? void 0 : _b.ok) === false) {
|
|
1181
|
+
res.status(400).json(response.result);
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
res.json(response.result);
|
|
1185
|
+
}));
|
|
1186
|
+
api.post('/api/windows/:sessionId/setup-campaign', (req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
1187
|
+
var _a, _b;
|
|
1188
|
+
const sessionId = String((_a = req.params.sessionId) !== null && _a !== void 0 ? _a : '').trim();
|
|
1189
|
+
if (!sessionId) {
|
|
1190
|
+
res.status(400).json({ ok: false, error: 'Session id is required.' });
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
const response = yield callAgentRendererApi(sessionId, 'setupCampaignWorkspace', req.body);
|
|
1194
|
+
if (!response.ok) {
|
|
1195
|
+
res.status(response.status).json({ ok: false, error: response.error });
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
1198
|
+
if (((_b = response.result) === null || _b === void 0 ? void 0 : _b.ok) === false) {
|
|
1199
|
+
res.status(400).json(response.result);
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1202
|
+
res.json(response.result);
|
|
1203
|
+
}));
|
|
1204
|
+
api.post('/api/windows/:sessionId/catalog/groups', (req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
1205
|
+
var _a, _b;
|
|
1206
|
+
const sessionId = String((_a = req.params.sessionId) !== null && _a !== void 0 ? _a : '').trim();
|
|
1207
|
+
if (!sessionId) {
|
|
1208
|
+
res.status(400).json({ ok: false, error: 'Session id is required.' });
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
const response = yield callAgentRendererApi(sessionId, 'createCampaignGroup', req.body);
|
|
1212
|
+
if (!response.ok) {
|
|
1213
|
+
res.status(response.status).json({ ok: false, error: response.error });
|
|
1214
|
+
return;
|
|
1215
|
+
}
|
|
1216
|
+
if (((_b = response.result) === null || _b === void 0 ? void 0 : _b.ok) === false) {
|
|
1217
|
+
res.status(400).json(response.result);
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
res.json(response.result);
|
|
1221
|
+
}));
|
|
1222
|
+
api.post('/api/windows/:sessionId/catalog/workspaces', (req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
1223
|
+
var _a, _b;
|
|
1224
|
+
const sessionId = String((_a = req.params.sessionId) !== null && _a !== void 0 ? _a : '').trim();
|
|
1225
|
+
if (!sessionId) {
|
|
1226
|
+
res.status(400).json({ ok: false, error: 'Session id is required.' });
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
const response = yield callAgentRendererApi(sessionId, 'createWorkspaceFolder', req.body);
|
|
1230
|
+
if (!response.ok) {
|
|
1231
|
+
res.status(response.status).json({ ok: false, error: response.error });
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
if (((_b = response.result) === null || _b === void 0 ? void 0 : _b.ok) === false) {
|
|
1235
|
+
res.status(400).json(response.result);
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
res.json(response.result);
|
|
1239
|
+
}));
|
|
1240
|
+
api.post('/api/windows/:sessionId/generate', (req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
1241
|
+
var _a, _b;
|
|
1242
|
+
const sessionId = String((_a = req.params.sessionId) !== null && _a !== void 0 ? _a : '').trim();
|
|
1243
|
+
if (!sessionId) {
|
|
1244
|
+
res.status(400).json({ ok: false, error: 'Session id is required.' });
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
const body = req.body && typeof req.body === 'object' ? req.body : {};
|
|
1248
|
+
const response = yield callAgentRendererApi(sessionId, 'startGeneration', body);
|
|
1249
|
+
if (!response.ok) {
|
|
1250
|
+
res.status(response.status).json({ ok: false, error: response.error });
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1253
|
+
if (((_b = response.result) === null || _b === void 0 ? void 0 : _b.ok) === false) {
|
|
1254
|
+
res.status(400).json(response.result);
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1257
|
+
res.json(response.result);
|
|
1258
|
+
}));
|
|
1259
|
+
api.post('/api/windows/:sessionId/geo-block', (req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
1260
|
+
var _a, _b;
|
|
1261
|
+
const sessionId = String((_a = req.params.sessionId) !== null && _a !== void 0 ? _a : '').trim();
|
|
1262
|
+
if (!sessionId) {
|
|
1263
|
+
res.status(400).json({ ok: false, error: 'Session id is required.' });
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
const response = yield callAgentRendererApi(sessionId, 'fillGeoBlock', req.body);
|
|
1267
|
+
if (!response.ok) {
|
|
1268
|
+
res.status(response.status).json({ ok: false, error: response.error });
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
if (((_b = response.result) === null || _b === void 0 ? void 0 : _b.ok) === false) {
|
|
1272
|
+
res.status(400).json(response.result);
|
|
1273
|
+
return;
|
|
1274
|
+
}
|
|
1275
|
+
res.json(response.result);
|
|
1276
|
+
}));
|
|
1277
|
+
api.get('/api/windows/:sessionId/creative-selections', (req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
1278
|
+
var _a;
|
|
1279
|
+
const sessionId = String((_a = req.params.sessionId) !== null && _a !== void 0 ? _a : '').trim();
|
|
1280
|
+
if (!sessionId) {
|
|
1281
|
+
res.status(400).json({ ok: false, error: 'Session id is required.' });
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
const response = yield callAgentRendererApi(sessionId, 'getCreativeSelections');
|
|
1285
|
+
if (!response.ok) {
|
|
1286
|
+
res.status(response.status).json({ ok: false, error: response.error });
|
|
1287
|
+
return;
|
|
1288
|
+
}
|
|
1289
|
+
res.json({ ok: true, selections: response.result });
|
|
1290
|
+
}));
|
|
1291
|
+
api.post('/api/windows/:sessionId/creative-selections', (req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
1292
|
+
var _a, _b;
|
|
1293
|
+
const sessionId = String((_a = req.params.sessionId) !== null && _a !== void 0 ? _a : '').trim();
|
|
1294
|
+
if (!sessionId) {
|
|
1295
|
+
res.status(400).json({ ok: false, error: 'Session id is required.' });
|
|
1296
|
+
return;
|
|
1297
|
+
}
|
|
1298
|
+
const response = yield callAgentRendererApi(sessionId, 'setCreativeSelections', req.body);
|
|
1299
|
+
if (!response.ok) {
|
|
1300
|
+
res.status(response.status).json({ ok: false, error: response.error });
|
|
1301
|
+
return;
|
|
1302
|
+
}
|
|
1303
|
+
if (((_b = response.result) === null || _b === void 0 ? void 0 : _b.ok) === false) {
|
|
1304
|
+
res.status(400).json(response.result);
|
|
1305
|
+
return;
|
|
1306
|
+
}
|
|
1307
|
+
res.json(response.result);
|
|
1308
|
+
}));
|
|
1309
|
+
api.get('/api/windows/:sessionId/capabilities', (req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
1310
|
+
var _a;
|
|
1311
|
+
const sessionId = String((_a = req.params.sessionId) !== null && _a !== void 0 ? _a : '').trim();
|
|
1312
|
+
const response = yield callAgentRendererApi(sessionId, 'getCapabilities');
|
|
1313
|
+
if (!response.ok) {
|
|
1314
|
+
res.status(response.status).json({ ok: false, error: response.error });
|
|
1315
|
+
return;
|
|
1316
|
+
}
|
|
1317
|
+
res.json({ ok: true, capabilities: response.result });
|
|
1318
|
+
}));
|
|
1319
|
+
api.get('/api/windows/:sessionId/validation', (req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
1320
|
+
var _a;
|
|
1321
|
+
const sessionId = String((_a = req.params.sessionId) !== null && _a !== void 0 ? _a : '').trim();
|
|
1322
|
+
const response = yield callAgentRendererApi(sessionId, 'getValidation');
|
|
1323
|
+
if (!response.ok) {
|
|
1324
|
+
res.status(response.status).json({ ok: false, error: response.error });
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
res.json({ ok: true, validation: response.result });
|
|
1328
|
+
}));
|
|
1329
|
+
api.get('/api/windows/:sessionId/jobs', (req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
1330
|
+
var _a;
|
|
1331
|
+
const sessionId = String((_a = req.params.sessionId) !== null && _a !== void 0 ? _a : '').trim();
|
|
1332
|
+
const response = yield callAgentRendererApi(sessionId, 'getJobStatus', {});
|
|
1333
|
+
if (!response.ok) {
|
|
1334
|
+
res.status(response.status).json({ ok: false, error: response.error });
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
res.json(response.result);
|
|
1338
|
+
}));
|
|
1339
|
+
api.get('/api/windows/:sessionId/jobs/:jobId', (req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
1340
|
+
var _a, _b;
|
|
1341
|
+
const sessionId = String((_a = req.params.sessionId) !== null && _a !== void 0 ? _a : '').trim();
|
|
1342
|
+
const response = yield callAgentRendererApi(sessionId, 'getJobStatus', { jobId: req.params.jobId });
|
|
1343
|
+
if (!response.ok) {
|
|
1344
|
+
res.status(response.status).json({ ok: false, error: response.error });
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1347
|
+
if (((_b = response.result) === null || _b === void 0 ? void 0 : _b.ok) === false) {
|
|
1348
|
+
res.status(404).json(response.result);
|
|
1349
|
+
return;
|
|
1350
|
+
}
|
|
1351
|
+
res.json(response.result);
|
|
1352
|
+
}));
|
|
1353
|
+
api.get('/api/windows/:sessionId/generated', (req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
1354
|
+
var _a;
|
|
1355
|
+
const sessionId = String((_a = req.params.sessionId) !== null && _a !== void 0 ? _a : '').trim();
|
|
1356
|
+
const response = yield callAgentRendererApi(sessionId, 'getGenerated');
|
|
1357
|
+
if (!response.ok) {
|
|
1358
|
+
res.status(response.status).json({ ok: false, error: response.error });
|
|
1359
|
+
return;
|
|
1360
|
+
}
|
|
1361
|
+
res.json({ ok: true, generated: response.result });
|
|
1362
|
+
}));
|
|
1363
|
+
api.patch('/api/windows/:sessionId/generated', (req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
1364
|
+
var _a, _b;
|
|
1365
|
+
const sessionId = String((_a = req.params.sessionId) !== null && _a !== void 0 ? _a : '').trim();
|
|
1366
|
+
const response = yield callAgentRendererApi(sessionId, 'patchGenerated', req.body);
|
|
1367
|
+
if (!response.ok) {
|
|
1368
|
+
res.status(response.status).json({ ok: false, error: response.error });
|
|
1369
|
+
return;
|
|
1370
|
+
}
|
|
1371
|
+
if (((_b = response.result) === null || _b === void 0 ? void 0 : _b.ok) === false) {
|
|
1372
|
+
res.status(400).json(response.result);
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
res.json(response.result);
|
|
1376
|
+
}));
|
|
1377
|
+
api.get('/api/windows/:sessionId/logs', (req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
1378
|
+
var _a;
|
|
1379
|
+
const sessionId = String((_a = req.params.sessionId) !== null && _a !== void 0 ? _a : '').trim();
|
|
1380
|
+
const response = yield callAgentRendererApi(sessionId, 'getLogs');
|
|
1381
|
+
if (!response.ok) {
|
|
1382
|
+
res.status(response.status).json({ ok: false, error: response.error });
|
|
1383
|
+
return;
|
|
1384
|
+
}
|
|
1385
|
+
res.json({ ok: true, logs: response.result });
|
|
1386
|
+
}));
|
|
1387
|
+
api.get('/api/spec', (_req, res) => {
|
|
1388
|
+
const address = agentApiServer === null || agentApiServer === void 0 ? void 0 : agentApiServer.address();
|
|
1389
|
+
const port = address && typeof address !== 'string' ? address.port : AGENT_API_PORT;
|
|
1390
|
+
res.json(getAgentApiSpec(port));
|
|
1391
|
+
});
|
|
1392
|
+
agentApiServer = api.listen(AGENT_API_PORT, AGENT_API_HOST, () => {
|
|
1393
|
+
console.log(`[Agent API] Listening on http://${AGENT_API_HOST}:${AGENT_API_PORT}`);
|
|
1394
|
+
});
|
|
1395
|
+
agentApiServer.on('error', (err) => {
|
|
1396
|
+
console.error('[Agent API] Failed to start:', err);
|
|
1397
|
+
});
|
|
1398
|
+
}
|
|
1399
|
+
function broadcastWorkspaceWindowsUpdated() {
|
|
1400
|
+
const list = getWorkspaceWindows();
|
|
1401
|
+
for (const win of electron_1.BrowserWindow.getAllWindows()) {
|
|
1402
|
+
if (!win.isDestroyed() && !win.webContents.isDestroyed()) {
|
|
1403
|
+
win.webContents.send('workspace-windows-updated', list);
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
function focusWindowByWebContentsId(webContentsId) {
|
|
1408
|
+
const win = electron_1.BrowserWindow.getAllWindows().find(w => w.webContents.id === webContentsId);
|
|
1409
|
+
if (!win || win.isDestroyed())
|
|
1410
|
+
return false;
|
|
1411
|
+
if (win.isMinimized())
|
|
1412
|
+
win.restore();
|
|
1413
|
+
win.show();
|
|
1414
|
+
win.focus();
|
|
1415
|
+
return true;
|
|
1416
|
+
}
|
|
1417
|
+
function cleanupWindowFolder(webContentsId) {
|
|
1418
|
+
const folderId = windowFolders.get(webContentsId);
|
|
1419
|
+
windowFolderPaths.delete(webContentsId);
|
|
1420
|
+
if (!folderId)
|
|
1421
|
+
return;
|
|
1422
|
+
windowFolders.delete(webContentsId);
|
|
1423
|
+
if (folderOwners.get(folderId) === webContentsId) {
|
|
1424
|
+
folderOwners.delete(folderId);
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
function broadcastConfigUpdated(config) {
|
|
1428
|
+
for (const win of electron_1.BrowserWindow.getAllWindows()) {
|
|
1429
|
+
if (!win.isDestroyed() && !win.webContents.isDestroyed()) {
|
|
1430
|
+
win.webContents.send('config-updated', config);
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
59
1434
|
function toggleDevToolsForWindow(win) {
|
|
60
1435
|
if (!win || win.isDestroyed()) {
|
|
61
1436
|
console.warn('[DevTools] No BrowserWindow available');
|
|
@@ -128,11 +1503,31 @@ function buildAppMenu() {
|
|
|
128
1503
|
template.push({ role: 'appMenu' });
|
|
129
1504
|
// Cut/Copy/Paste/Select All — без этого Cmd+V в полях иногда не работает (Chromium + системное меню).
|
|
130
1505
|
template.push({ role: 'editMenu' });
|
|
1506
|
+
template.push({
|
|
1507
|
+
label: 'File',
|
|
1508
|
+
submenu: [
|
|
1509
|
+
{
|
|
1510
|
+
label: 'New Window',
|
|
1511
|
+
accelerator: 'CmdOrCtrl+N',
|
|
1512
|
+
click: () => createWindow(),
|
|
1513
|
+
},
|
|
1514
|
+
{ type: 'separator' },
|
|
1515
|
+
{ role: 'close' },
|
|
1516
|
+
],
|
|
1517
|
+
});
|
|
131
1518
|
}
|
|
132
1519
|
else {
|
|
133
1520
|
template.push({
|
|
134
1521
|
label: 'File',
|
|
135
|
-
submenu: [
|
|
1522
|
+
submenu: [
|
|
1523
|
+
{
|
|
1524
|
+
label: 'New Window',
|
|
1525
|
+
accelerator: 'Ctrl+N',
|
|
1526
|
+
click: () => createWindow(),
|
|
1527
|
+
},
|
|
1528
|
+
{ type: 'separator' },
|
|
1529
|
+
{ role: 'quit' },
|
|
1530
|
+
],
|
|
136
1531
|
});
|
|
137
1532
|
template.push({
|
|
138
1533
|
label: 'Edit',
|
|
@@ -167,10 +1562,11 @@ function buildAppMenu() {
|
|
|
167
1562
|
});
|
|
168
1563
|
electron_1.Menu.setApplicationMenu(electron_1.Menu.buildFromTemplate(template));
|
|
169
1564
|
}
|
|
170
|
-
function createWindow() {
|
|
171
|
-
|
|
1565
|
+
function createWindow(workspaceId = createWorkspaceId()) {
|
|
1566
|
+
const win = new electron_1.BrowserWindow({
|
|
172
1567
|
width: 1000,
|
|
173
1568
|
height: 800,
|
|
1569
|
+
title: APP_WINDOW_TITLE,
|
|
174
1570
|
backgroundColor: '#121212', // Set to dark by default, will be corrected by preload if light
|
|
175
1571
|
show: false, // Don't show until ready to prevent flash
|
|
176
1572
|
webPreferences: {
|
|
@@ -180,7 +1576,25 @@ function createWindow() {
|
|
|
180
1576
|
sandbox: false
|
|
181
1577
|
},
|
|
182
1578
|
});
|
|
183
|
-
|
|
1579
|
+
mainWindow = win;
|
|
1580
|
+
windows.add(win);
|
|
1581
|
+
windowWorkspaces.set(win.webContents.id, workspaceId);
|
|
1582
|
+
windowTitles.set(win.webContents.id, 'Новый оффер');
|
|
1583
|
+
broadcastWorkspaceWindowsUpdated();
|
|
1584
|
+
win.on('closed', () => {
|
|
1585
|
+
var _a;
|
|
1586
|
+
closingWindowIds.delete(win.webContents.id);
|
|
1587
|
+
windows.delete(win);
|
|
1588
|
+
cleanupWindowFolder(win.webContents.id);
|
|
1589
|
+
windowWorkspaces.delete(win.webContents.id);
|
|
1590
|
+
windowTitles.delete(win.webContents.id);
|
|
1591
|
+
if (mainWindow === win) {
|
|
1592
|
+
mainWindow = (_a = electron_1.BrowserWindow.getAllWindows()[0]) !== null && _a !== void 0 ? _a : null;
|
|
1593
|
+
}
|
|
1594
|
+
broadcastWorkspaceWindowsUpdated();
|
|
1595
|
+
});
|
|
1596
|
+
win.on('focus', () => broadcastWorkspaceWindowsUpdated());
|
|
1597
|
+
win.on('show', () => broadcastWorkspaceWindowsUpdated());
|
|
184
1598
|
// Toggles from main: works when the page/React failed — handled before the renderer consumes the key.
|
|
185
1599
|
// F12, Cmd+Shift+D (mac) / Ctrl+Shift+D (win/linux); also View → Toggle Developer Tools / Cmd+Option+I (see menu).
|
|
186
1600
|
win.webContents.on('before-input-event', (event, input) => {
|
|
@@ -237,201 +1651,350 @@ function createWindow() {
|
|
|
237
1651
|
});
|
|
238
1652
|
});
|
|
239
1653
|
// We need to point to the webpack output html
|
|
240
|
-
win.loadFile(path.join(__dirname, 'index.html')
|
|
1654
|
+
win.loadFile(path.join(__dirname, 'index.html'), {
|
|
1655
|
+
query: { workspaceId },
|
|
1656
|
+
});
|
|
241
1657
|
// DevTools disabled by default as requested
|
|
242
|
-
//
|
|
1658
|
+
// win.webContents.openDevTools();
|
|
243
1659
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
const userDataPath = electron_1.app.getPath('userData');
|
|
250
|
-
if (!fs.existsSync(userDataPath)) {
|
|
251
|
-
fs.mkdirSync(userDataPath, { recursive: true });
|
|
252
|
-
}
|
|
253
|
-
const configPath = path.join(userDataPath, 'config.json');
|
|
254
|
-
electron_1.ipcMain.handle('save-config', (event, config) => __awaiter(void 0, void 0, void 0, function* () {
|
|
255
|
-
try {
|
|
256
|
-
fs.writeFileSync(configPath, JSON.stringify(config));
|
|
257
|
-
return true;
|
|
1660
|
+
if (gotSingleInstanceLock) {
|
|
1661
|
+
electron_1.app.on('second-instance', () => {
|
|
1662
|
+
if (electron_1.app.isReady()) {
|
|
1663
|
+
createWindow();
|
|
1664
|
+
return;
|
|
258
1665
|
}
|
|
259
|
-
|
|
260
|
-
|
|
1666
|
+
electron_1.app.whenReady().then(() => createWindow()).catch(err => {
|
|
1667
|
+
console.error('[Main] Failed to open window for second instance:', err);
|
|
1668
|
+
});
|
|
1669
|
+
});
|
|
1670
|
+
}
|
|
1671
|
+
if (gotSingleInstanceLock)
|
|
1672
|
+
electron_1.app.whenReady().then(() => {
|
|
1673
|
+
buildAppMenu();
|
|
1674
|
+
startAgentApiServer();
|
|
1675
|
+
electron_1.app.on('child-process-gone', (_event, details) => {
|
|
1676
|
+
console.error(`[Electron] child-process-gone: type=${details.type}, reason=${details.reason}, exitCode=${details.exitCode}`);
|
|
1677
|
+
});
|
|
1678
|
+
const userDataPath = electron_1.app.getPath('userData');
|
|
1679
|
+
if (!fs.existsSync(userDataPath)) {
|
|
1680
|
+
fs.mkdirSync(userDataPath, { recursive: true });
|
|
261
1681
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
1682
|
+
const configPath = path.join(userDataPath, 'config.json');
|
|
1683
|
+
const readConfig = () => {
|
|
1684
|
+
try {
|
|
1685
|
+
if (!fs.existsSync(configPath))
|
|
1686
|
+
return {};
|
|
266
1687
|
const content = fs.readFileSync(configPath, 'utf-8');
|
|
267
1688
|
return JSON.parse(content);
|
|
268
1689
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
return
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
return
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
1690
|
+
catch (_a) {
|
|
1691
|
+
return {};
|
|
1692
|
+
}
|
|
1693
|
+
};
|
|
1694
|
+
const writeConfig = (config) => {
|
|
1695
|
+
try {
|
|
1696
|
+
fs.writeFileSync(configPath, JSON.stringify(config));
|
|
1697
|
+
broadcastConfigUpdated(config);
|
|
1698
|
+
return true;
|
|
1699
|
+
}
|
|
1700
|
+
catch (_a) {
|
|
1701
|
+
return false;
|
|
1702
|
+
}
|
|
1703
|
+
};
|
|
1704
|
+
const getWorkspaceSnapshots = () => {
|
|
1705
|
+
const config = readConfig();
|
|
1706
|
+
return Array.isArray(config.workspaceSnapshots)
|
|
1707
|
+
? config.workspaceSnapshots.filter((item) => Boolean(item && typeof item === 'object' && typeof item.id === 'string'))
|
|
1708
|
+
: [];
|
|
1709
|
+
};
|
|
1710
|
+
electron_1.ipcMain.handle('save-config', (event, config) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1711
|
+
return writeConfig(config);
|
|
1712
|
+
}));
|
|
1713
|
+
electron_1.ipcMain.handle('load-config', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
1714
|
+
return readConfig();
|
|
1715
|
+
}));
|
|
1716
|
+
electron_1.ipcMain.handle('load-workspace-snapshot', (_event, workspaceId) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1717
|
+
var _a;
|
|
1718
|
+
const id = String(workspaceId !== null && workspaceId !== void 0 ? workspaceId : '').trim();
|
|
1719
|
+
if (!id)
|
|
1720
|
+
return null;
|
|
1721
|
+
return (_a = getWorkspaceSnapshots().find(snapshot => snapshot.id === id)) !== null && _a !== void 0 ? _a : null;
|
|
1722
|
+
}));
|
|
1723
|
+
electron_1.ipcMain.handle('save-workspace-snapshot', (_event, snapshot) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1724
|
+
var _a;
|
|
1725
|
+
const id = String((_a = snapshot === null || snapshot === void 0 ? void 0 : snapshot.id) !== null && _a !== void 0 ? _a : '').trim();
|
|
1726
|
+
if (!id)
|
|
1727
|
+
return { ok: false };
|
|
1728
|
+
const config = readConfig();
|
|
1729
|
+
const current = Array.isArray(config.workspaceSnapshots) ? config.workspaceSnapshots : [];
|
|
1730
|
+
const nextSnapshot = Object.assign(Object.assign({}, snapshot), { id, updatedAt: typeof snapshot.updatedAt === 'string' ? snapshot.updatedAt : new Date().toISOString() });
|
|
1731
|
+
const next = [
|
|
1732
|
+
nextSnapshot,
|
|
1733
|
+
...current.filter((item) => (item === null || item === void 0 ? void 0 : item.id) !== id),
|
|
1734
|
+
].slice(0, 12);
|
|
1735
|
+
const ok = writeConfig(Object.assign(Object.assign({}, config), { workspaceSnapshots: next }));
|
|
1736
|
+
return { ok };
|
|
1737
|
+
}));
|
|
1738
|
+
electron_1.ipcMain.handle('register-workspace-folder', (event, rawFolderId) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1739
|
+
const webContentsId = event.sender.id;
|
|
1740
|
+
const folderId = String(rawFolderId !== null && rawFolderId !== void 0 ? rawFolderId : '').trim();
|
|
1741
|
+
if (!folderId) {
|
|
1742
|
+
cleanupWindowFolder(webContentsId);
|
|
1743
|
+
return { ok: true };
|
|
1744
|
+
}
|
|
1745
|
+
const currentOwner = folderOwners.get(folderId);
|
|
1746
|
+
if (currentOwner && currentOwner !== webContentsId) {
|
|
1747
|
+
focusWindowByWebContentsId(currentOwner);
|
|
1748
|
+
return {
|
|
1749
|
+
ok: false,
|
|
1750
|
+
reason: 'occupied',
|
|
1751
|
+
folderId,
|
|
1752
|
+
};
|
|
1753
|
+
}
|
|
1754
|
+
cleanupWindowFolder(webContentsId);
|
|
1755
|
+
folderOwners.set(folderId, webContentsId);
|
|
1756
|
+
windowFolders.set(webContentsId, folderId);
|
|
1757
|
+
broadcastWorkspaceWindowsUpdated();
|
|
1758
|
+
return { ok: true };
|
|
1759
|
+
}));
|
|
1760
|
+
electron_1.ipcMain.handle('unregister-workspace-folder', (event, rawFolderId) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1761
|
+
const webContentsId = event.sender.id;
|
|
1762
|
+
const folderId = rawFolderId == null ? null : String(rawFolderId).trim();
|
|
1763
|
+
if (!folderId || windowFolders.get(webContentsId) === folderId) {
|
|
1764
|
+
cleanupWindowFolder(webContentsId);
|
|
1765
|
+
}
|
|
1766
|
+
broadcastWorkspaceWindowsUpdated();
|
|
1767
|
+
return { ok: true };
|
|
1768
|
+
}));
|
|
1769
|
+
electron_1.ipcMain.handle('new-window', (_event, workspaceId) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1770
|
+
createWindow(String(workspaceId !== null && workspaceId !== void 0 ? workspaceId : '').trim() || createWorkspaceId());
|
|
1771
|
+
return { ok: true };
|
|
1772
|
+
}));
|
|
1773
|
+
electron_1.ipcMain.handle('set-workspace-window-meta', (event, payload) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1774
|
+
var _a, _b, _c;
|
|
1775
|
+
const webContentsId = event.sender.id;
|
|
1776
|
+
const title = String((_a = payload === null || payload === void 0 ? void 0 : payload.title) !== null && _a !== void 0 ? _a : '').trim();
|
|
1777
|
+
if (title) {
|
|
1778
|
+
windowTitles.set(webContentsId, title);
|
|
1779
|
+
const win = electron_1.BrowserWindow.fromWebContents(event.sender);
|
|
1780
|
+
if (win && !win.isDestroyed()) {
|
|
1781
|
+
win.setTitle(`${APP_WINDOW_TITLE} — ${title}`);
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
const folderId = String((_b = payload === null || payload === void 0 ? void 0 : payload.folderId) !== null && _b !== void 0 ? _b : '').trim();
|
|
1785
|
+
if (folderId) {
|
|
1786
|
+
windowFolders.set(webContentsId, folderId);
|
|
1787
|
+
}
|
|
1788
|
+
const folderPath = String((_c = payload === null || payload === void 0 ? void 0 : payload.folderPath) !== null && _c !== void 0 ? _c : '').trim();
|
|
1789
|
+
if (folderPath) {
|
|
1790
|
+
windowFolderPaths.set(webContentsId, folderPath);
|
|
1791
|
+
}
|
|
1792
|
+
else {
|
|
1793
|
+
windowFolderPaths.delete(webContentsId);
|
|
1794
|
+
}
|
|
1795
|
+
broadcastWorkspaceWindowsUpdated();
|
|
1796
|
+
return { ok: true };
|
|
1797
|
+
}));
|
|
1798
|
+
electron_1.ipcMain.handle('list-workspace-windows', () => __awaiter(void 0, void 0, void 0, function* () { return getWorkspaceWindows(); }));
|
|
1799
|
+
electron_1.ipcMain.handle('focus-workspace-window', (_event, webContentsId) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1800
|
+
return ({
|
|
1801
|
+
ok: focusWindowByWebContentsId(Number(webContentsId)),
|
|
1802
|
+
});
|
|
1803
|
+
}));
|
|
1804
|
+
electron_1.ipcMain.handle('close-workspace-window', (_event, webContentsId) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1805
|
+
const id = Number(webContentsId);
|
|
1806
|
+
const win = electron_1.BrowserWindow.getAllWindows().find(w => w.webContents.id === id);
|
|
1807
|
+
if (!win || win.isDestroyed())
|
|
1808
|
+
return { ok: false };
|
|
1809
|
+
const workspaceId = windowWorkspaces.get(id);
|
|
1810
|
+
if (workspaceId) {
|
|
1811
|
+
const config = readConfig();
|
|
1812
|
+
const current = Array.isArray(config.workspaceSnapshots) ? config.workspaceSnapshots : [];
|
|
1813
|
+
const next = current.filter((item) => (item === null || item === void 0 ? void 0 : item.id) !== workspaceId);
|
|
1814
|
+
if (next.length !== current.length) {
|
|
1815
|
+
writeConfig(Object.assign(Object.assign({}, config), { workspaceSnapshots: next }));
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
closingWindowIds.add(id);
|
|
1819
|
+
broadcastWorkspaceWindowsUpdated();
|
|
1820
|
+
win.destroy();
|
|
1821
|
+
setImmediate(() => broadcastWorkspaceWindowsUpdated());
|
|
1822
|
+
return { ok: true };
|
|
1823
|
+
}));
|
|
1824
|
+
electron_1.ipcMain.handle('telegram-send-test', (_event, payload) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1825
|
+
var _a, _b;
|
|
1826
|
+
const botToken = String((_a = payload === null || payload === void 0 ? void 0 : payload.botToken) !== null && _a !== void 0 ? _a : '').trim();
|
|
1827
|
+
const chatId = String((_b = payload === null || payload === void 0 ? void 0 : payload.chatId) !== null && _b !== void 0 ? _b : '').trim();
|
|
1828
|
+
if (!botToken || !chatId) {
|
|
1829
|
+
return { ok: false, error: 'Укажите токен бота и ID чата.' };
|
|
1830
|
+
}
|
|
1831
|
+
return telegramSendMessageViaBot(botToken, chatId, 'Docs Combiner — тест оповещений.');
|
|
1832
|
+
}));
|
|
1833
|
+
electron_1.ipcMain.handle('telegram-send-message', (_event, payload) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1834
|
+
var _a, _b, _c;
|
|
1835
|
+
const botToken = String((_a = payload === null || payload === void 0 ? void 0 : payload.botToken) !== null && _a !== void 0 ? _a : '').trim();
|
|
1836
|
+
const chatId = String((_b = payload === null || payload === void 0 ? void 0 : payload.chatId) !== null && _b !== void 0 ? _b : '').trim();
|
|
1837
|
+
const text = String((_c = payload === null || payload === void 0 ? void 0 : payload.text) !== null && _c !== void 0 ? _c : '');
|
|
1838
|
+
if (!botToken || !chatId) {
|
|
1839
|
+
return { ok: false, error: 'Укажите токен бота и ID чата.' };
|
|
1840
|
+
}
|
|
1841
|
+
if (!text.trim()) {
|
|
1842
|
+
return { ok: false, error: 'Пустой текст сообщения.' };
|
|
1843
|
+
}
|
|
1844
|
+
return telegramSendMessageViaBot(botToken, chatId, text);
|
|
1845
|
+
}));
|
|
1846
|
+
electron_1.ipcMain.handle('save-file', (event, content, filename) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1847
|
+
const { canceled, filePath } = yield electron_1.dialog.showSaveDialog({
|
|
1848
|
+
defaultPath: filename,
|
|
1849
|
+
});
|
|
1850
|
+
if (canceled || !filePath)
|
|
1851
|
+
return false;
|
|
1852
|
+
fs.writeFileSync(filePath, content);
|
|
1853
|
+
return true;
|
|
1854
|
+
}));
|
|
1855
|
+
electron_1.ipcMain.handle('start-auth', (event, clientId) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1856
|
+
abortPendingGoogleAuth('AUTH_SUPERSEDED');
|
|
1857
|
+
return new Promise((resolve, reject) => {
|
|
1858
|
+
const server = http.createServer((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1859
|
+
try {
|
|
1860
|
+
if (!req.url)
|
|
1861
|
+
return;
|
|
1862
|
+
const parsedUrl = url.parse(req.url, true);
|
|
1863
|
+
// Allow any path for flexibility
|
|
1864
|
+
const query = parsedUrl.query;
|
|
1865
|
+
if (query.code) {
|
|
1866
|
+
const address = server.address();
|
|
1867
|
+
const port = address && typeof address !== 'string' ? address.port : 0;
|
|
1868
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
1869
|
+
res.end('<h1>Authentication successful!</h1><p>You can close this window and return to the app.</p><script>window.close();</script>');
|
|
1870
|
+
pendingGoogleAuth = null;
|
|
1871
|
+
try {
|
|
1872
|
+
server.close();
|
|
1873
|
+
}
|
|
1874
|
+
catch (_a) {
|
|
1875
|
+
/* ignore */
|
|
1876
|
+
}
|
|
1877
|
+
resolve({ code: query.code, redirectUri: `http://127.0.0.1:${port}` });
|
|
1878
|
+
}
|
|
1879
|
+
else if (query.error) {
|
|
1880
|
+
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
1881
|
+
res.end('Authentication failed.');
|
|
1882
|
+
pendingGoogleAuth = null;
|
|
1883
|
+
try {
|
|
1884
|
+
server.close();
|
|
1885
|
+
}
|
|
1886
|
+
catch (_b) {
|
|
1887
|
+
/* ignore */
|
|
1888
|
+
}
|
|
1889
|
+
reject(new Error(query.error));
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
catch (e) {
|
|
321
1893
|
pendingGoogleAuth = null;
|
|
322
1894
|
try {
|
|
323
1895
|
server.close();
|
|
324
1896
|
}
|
|
325
|
-
catch (
|
|
1897
|
+
catch (_c) {
|
|
326
1898
|
/* ignore */
|
|
327
1899
|
}
|
|
328
|
-
|
|
1900
|
+
reject(e instanceof Error ? e : new Error(String(e)));
|
|
329
1901
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
1902
|
+
}));
|
|
1903
|
+
server.on('error', (err) => {
|
|
1904
|
+
if ((pendingGoogleAuth === null || pendingGoogleAuth === void 0 ? void 0 : pendingGoogleAuth.server) === server) {
|
|
333
1905
|
pendingGoogleAuth = null;
|
|
1906
|
+
}
|
|
1907
|
+
reject(err);
|
|
1908
|
+
});
|
|
1909
|
+
server.listen(0, '127.0.0.1', () => {
|
|
1910
|
+
const address = server.address();
|
|
1911
|
+
if (!address || typeof address === 'string') {
|
|
334
1912
|
try {
|
|
335
1913
|
server.close();
|
|
336
1914
|
}
|
|
337
|
-
catch (
|
|
1915
|
+
catch (_a) {
|
|
338
1916
|
/* ignore */
|
|
339
1917
|
}
|
|
340
|
-
reject(new Error(
|
|
341
|
-
|
|
342
|
-
}
|
|
343
|
-
catch (e) {
|
|
344
|
-
pendingGoogleAuth = null;
|
|
345
|
-
try {
|
|
346
|
-
server.close();
|
|
347
|
-
}
|
|
348
|
-
catch (_c) {
|
|
349
|
-
/* ignore */
|
|
1918
|
+
reject(new Error('Failed to get server port'));
|
|
1919
|
+
return;
|
|
350
1920
|
}
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
pendingGoogleAuth =
|
|
357
|
-
|
|
358
|
-
|
|
1921
|
+
const port = address.port;
|
|
1922
|
+
const redirectUri = `http://127.0.0.1:${port}`;
|
|
1923
|
+
// Google Drive Scope: https://www.googleapis.com/auth/drive (Full Access)
|
|
1924
|
+
// Added prompt=consent to force new refresh token with correct scopes
|
|
1925
|
+
const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?access_type=offline&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fdrive&response_type=code&client_id=${clientId}&redirect_uri=${redirectUri}&prompt=consent`;
|
|
1926
|
+
pendingGoogleAuth = { authUrl, server, resolve, reject };
|
|
1927
|
+
electron_1.shell.openExternal(authUrl);
|
|
1928
|
+
});
|
|
359
1929
|
});
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
1930
|
+
}));
|
|
1931
|
+
electron_1.ipcMain.handle('reopen-pending-auth-browser', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
1932
|
+
if (!pendingGoogleAuth) {
|
|
1933
|
+
return { ok: false };
|
|
1934
|
+
}
|
|
1935
|
+
yield electron_1.shell.openExternal(pendingGoogleAuth.authUrl);
|
|
1936
|
+
return { ok: true };
|
|
1937
|
+
}));
|
|
1938
|
+
electron_1.ipcMain.handle('cancel-pending-auth', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
1939
|
+
if (!pendingGoogleAuth) {
|
|
1940
|
+
return { ok: false };
|
|
1941
|
+
}
|
|
1942
|
+
abortPendingGoogleAuth('LOGIN_CANCELLED');
|
|
1943
|
+
return { ok: true };
|
|
1944
|
+
}));
|
|
1945
|
+
electron_1.ipcMain.handle('open-external', (event, url) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1946
|
+
yield electron_1.shell.openExternal(url);
|
|
1947
|
+
}));
|
|
1948
|
+
electron_1.ipcMain.handle('log', (event, level, ...args) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1949
|
+
const timestamp = new Date().toISOString();
|
|
1950
|
+
const message = args.map(arg => {
|
|
1951
|
+
if (typeof arg === 'object') {
|
|
363
1952
|
try {
|
|
364
|
-
|
|
1953
|
+
return JSON.stringify(arg, null, 2);
|
|
365
1954
|
}
|
|
366
1955
|
catch (_a) {
|
|
367
|
-
|
|
1956
|
+
return String(arg);
|
|
368
1957
|
}
|
|
369
|
-
reject(new Error('Failed to get server port'));
|
|
370
|
-
return;
|
|
371
|
-
}
|
|
372
|
-
const port = address.port;
|
|
373
|
-
const redirectUri = `http://127.0.0.1:${port}`;
|
|
374
|
-
// Google Drive Scope: https://www.googleapis.com/auth/drive (Full Access)
|
|
375
|
-
// Added prompt=consent to force new refresh token with correct scopes
|
|
376
|
-
const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?access_type=offline&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fdrive&response_type=code&client_id=${clientId}&redirect_uri=${redirectUri}&prompt=consent`;
|
|
377
|
-
pendingGoogleAuth = { authUrl, server, resolve, reject };
|
|
378
|
-
electron_1.shell.openExternal(authUrl);
|
|
379
|
-
});
|
|
380
|
-
});
|
|
381
|
-
}));
|
|
382
|
-
electron_1.ipcMain.handle('reopen-pending-auth-browser', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
383
|
-
if (!pendingGoogleAuth) {
|
|
384
|
-
return { ok: false };
|
|
385
|
-
}
|
|
386
|
-
yield electron_1.shell.openExternal(pendingGoogleAuth.authUrl);
|
|
387
|
-
return { ok: true };
|
|
388
|
-
}));
|
|
389
|
-
electron_1.ipcMain.handle('cancel-pending-auth', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
390
|
-
if (!pendingGoogleAuth) {
|
|
391
|
-
return { ok: false };
|
|
392
|
-
}
|
|
393
|
-
abortPendingGoogleAuth('LOGIN_CANCELLED');
|
|
394
|
-
return { ok: true };
|
|
395
|
-
}));
|
|
396
|
-
electron_1.ipcMain.handle('open-external', (event, url) => __awaiter(void 0, void 0, void 0, function* () {
|
|
397
|
-
yield electron_1.shell.openExternal(url);
|
|
398
|
-
}));
|
|
399
|
-
electron_1.ipcMain.handle('log', (event, level, ...args) => __awaiter(void 0, void 0, void 0, function* () {
|
|
400
|
-
const timestamp = new Date().toISOString();
|
|
401
|
-
const message = args.map(arg => {
|
|
402
|
-
if (typeof arg === 'object') {
|
|
403
|
-
try {
|
|
404
|
-
return JSON.stringify(arg, null, 2);
|
|
405
|
-
}
|
|
406
|
-
catch (_a) {
|
|
407
|
-
return String(arg);
|
|
408
1958
|
}
|
|
1959
|
+
return String(arg);
|
|
1960
|
+
}).join(' ');
|
|
1961
|
+
const formattedMessage = `[${timestamp}] ${message}`;
|
|
1962
|
+
switch (level) {
|
|
1963
|
+
case 'log':
|
|
1964
|
+
case 'info':
|
|
1965
|
+
console.log(formattedMessage);
|
|
1966
|
+
break;
|
|
1967
|
+
case 'warn':
|
|
1968
|
+
console.warn(formattedMessage);
|
|
1969
|
+
break;
|
|
1970
|
+
case 'error':
|
|
1971
|
+
console.error(formattedMessage);
|
|
1972
|
+
break;
|
|
409
1973
|
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
case 'log':
|
|
415
|
-
case 'info':
|
|
416
|
-
console.log(formattedMessage);
|
|
417
|
-
break;
|
|
418
|
-
case 'warn':
|
|
419
|
-
console.warn(formattedMessage);
|
|
420
|
-
break;
|
|
421
|
-
case 'error':
|
|
422
|
-
console.error(formattedMessage);
|
|
423
|
-
break;
|
|
1974
|
+
}));
|
|
1975
|
+
const snapshotsToRestore = getWorkspaceSnapshots();
|
|
1976
|
+
if (snapshotsToRestore.length > 0) {
|
|
1977
|
+
snapshotsToRestore.forEach(snapshot => createWindow(snapshot.id));
|
|
424
1978
|
}
|
|
425
|
-
|
|
426
|
-
createWindow();
|
|
427
|
-
electron_1.app.on('activate', function () {
|
|
428
|
-
if (electron_1.BrowserWindow.getAllWindows().length === 0)
|
|
1979
|
+
else {
|
|
429
1980
|
createWindow();
|
|
1981
|
+
}
|
|
1982
|
+
electron_1.app.on('activate', function () {
|
|
1983
|
+
if (electron_1.BrowserWindow.getAllWindows().length === 0) {
|
|
1984
|
+
const snapshot = getWorkspaceSnapshots()[0];
|
|
1985
|
+
createWindow(snapshot === null || snapshot === void 0 ? void 0 : snapshot.id);
|
|
1986
|
+
}
|
|
1987
|
+
});
|
|
430
1988
|
});
|
|
431
|
-
});
|
|
432
1989
|
electron_1.app.on('window-all-closed', function () {
|
|
433
1990
|
electron_1.app.quit();
|
|
434
1991
|
});
|
|
1992
|
+
electron_1.app.on('before-quit', () => {
|
|
1993
|
+
if (agentApiServer) {
|
|
1994
|
+
agentApiServer.close();
|
|
1995
|
+
agentApiServer = null;
|
|
1996
|
+
}
|
|
1997
|
+
});
|
|
435
1998
|
process.on('uncaughtException', (err) => {
|
|
436
1999
|
console.error('[Main] uncaughtException:', err);
|
|
437
2000
|
});
|