docs-combiner 0.4.0 → 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 (52) 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 +316 -1214
  28. package/dist/models.js +17 -0
  29. package/dist/offerSettings.js +19 -0
  30. package/dist/preload.js +9 -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.map +1 -1
  35. package/dist/server/app.js +378 -0
  36. package/dist/server/auth.js +74 -0
  37. package/dist/server/contentJobs.js +367 -0
  38. package/dist/server/driveCatalog.js +122 -0
  39. package/dist/server/driveProxy.js +220 -0
  40. package/dist/server/flexcardMeta.js +29 -0
  41. package/dist/server/googleAuth.js +84 -0
  42. package/dist/server/imageAssets.js +87 -0
  43. package/dist/server/imageJobs.js +776 -0
  44. package/dist/server/index.js +38 -0
  45. package/dist/server/jobEvents.js +116 -0
  46. package/dist/server/jobs.js +358 -0
  47. package/dist/server/openRouterMeta.js +58 -0
  48. package/dist/server/spec.js +544 -0
  49. package/dist/server/storage.js +133 -0
  50. package/dist/server/telegram.js +53 -0
  51. package/dist/server/workspaceSnapshot.js +273 -0
  52. package/package.json +15 -3
package/dist/main.js CHANGED
@@ -51,6 +51,8 @@ const path = __importStar(require("path"));
51
51
  const fs = __importStar(require("fs"));
52
52
  const http = __importStar(require("http"));
53
53
  const url = __importStar(require("url"));
54
+ const routes_1 = require("./agentApi/routes");
55
+ const agentControlClaim_1 = require("./agentApi/agentControlClaim");
54
56
  let pendingGoogleAuth = null;
55
57
  let mainWindow = null;
56
58
  let agentApiServer = null;
@@ -61,6 +63,8 @@ const windowFolderPaths = new Map();
61
63
  const windowWorkspaces = new Map();
62
64
  const windowTitles = new Map();
63
65
  const closingWindowIds = new Set();
66
+ const closedWorkspaceIds = new Set();
67
+ const agentControlledSessions = new Set();
64
68
  const AGENT_API_HOST = '127.0.0.1';
65
69
  const DEFAULT_AGENT_API_PORT = 17321;
66
70
  const APP_WINDOW_TITLE = 'Комбайнер';
@@ -106,41 +110,87 @@ function getWorkspaceWindows() {
106
110
  };
107
111
  });
108
112
  }
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
- };
113
+ function isSessionAgentControlled(sessionId) {
114
+ return agentControlledSessions.has(sessionId.trim());
115
+ }
116
+ function setSessionAgentControlMode(sessionId, mode) {
117
+ const id = sessionId.trim();
118
+ if (!id)
119
+ return;
120
+ if (mode === 'agent') {
121
+ agentControlledSessions.add(id);
122
+ }
123
+ else {
124
+ agentControlledSessions.delete(id);
125
+ }
126
+ const win = getWindowByWorkspaceId(id);
127
+ if (win && !win.isDestroyed() && !win.webContents.isDestroyed()) {
128
+ win.webContents.send('agent-control-mode-changed', { mode });
129
+ syncWindowTitleForAgentControl(win, id);
130
+ }
131
+ }
132
+ function isAgentApiReadyInSession(sessionId) {
133
+ return __awaiter(this, void 0, void 0, function* () {
134
+ const win = getWindowByWorkspaceId(sessionId);
135
+ if (!win || win.isDestroyed() || win.webContents.isDestroyed())
136
+ return false;
137
+ try {
138
+ return (yield win.webContents.executeJavaScript('Boolean(window.__docsCombinerAgentApi)', true)) === true;
139
+ }
140
+ catch (_a) {
141
+ return false;
142
+ }
142
143
  });
143
144
  }
145
+ function syncWindowTitleForAgentControl(win, workspaceId) {
146
+ const webContentsId = win.webContents.id;
147
+ const folderLabel = windowTitles.get(webContentsId) || 'Новый оффер';
148
+ const agentPrefix = isSessionAgentControlled(workspaceId) ? '🤖 ' : '';
149
+ win.setTitle(`${agentPrefix}${APP_WINDOW_TITLE} — ${folderLabel}`);
150
+ }
151
+ function buildAgentWorkSessionInfo(win) {
152
+ const baseUrl = getAgentApiBaseUrl();
153
+ const sessionUrl = `${baseUrl}/api/windows/${encodeURIComponent(win.workspaceId)}`;
154
+ const details = [
155
+ win.title,
156
+ win.folderPath ? `Drive folder: ${win.folderPath}` : 'No Drive folder selected',
157
+ win.active ? 'active session' : 'background session',
158
+ isSessionAgentControlled(win.workspaceId) ? 'agent-controlled' : 'manual control',
159
+ ];
160
+ return {
161
+ id: win.workspaceId,
162
+ title: win.title,
163
+ description: details.join('; '),
164
+ folderId: win.folderId,
165
+ folderPath: win.folderPath,
166
+ active: win.active,
167
+ agentControlled: isSessionAgentControlled(win.workspaceId),
168
+ url: sessionUrl,
169
+ endpoints: {
170
+ info: sessionUrl,
171
+ update: sessionUrl,
172
+ setupCampaign: `${sessionUrl}/setup-campaign`,
173
+ geoBlock: `${sessionUrl}/geo-block`,
174
+ creativeSelections: `${sessionUrl}/creative-selections`,
175
+ approachMatrix: `${sessionUrl}/approach-matrix`,
176
+ capabilities: `${sessionUrl}/capabilities`,
177
+ validation: `${sessionUrl}/validation`,
178
+ generated: `${sessionUrl}/generated`,
179
+ logs: `${sessionUrl}/logs`,
180
+ jobs: `${sessionUrl}/jobs`,
181
+ createCampaignGroup: `${sessionUrl}/catalog/groups`,
182
+ createWorkspaceFolder: `${sessionUrl}/catalog/workspaces`,
183
+ generate: `${sessionUrl}/generate`,
184
+ agentControl: `${sessionUrl}/agent-control`,
185
+ actions: `${sessionUrl}/actions`,
186
+ regeneratePair: `${sessionUrl}/actions/regenerate-pair`,
187
+ regenerateImages: `${sessionUrl}/actions/regenerate-images`,
188
+ },
189
+ };
190
+ }
191
+ function getAgentWorkSessions() {
192
+ return getWorkspaceWindows().map(buildAgentWorkSessionInfo);
193
+ }
144
194
  function getWindowByWorkspaceId(workspaceId) {
145
195
  var _a;
146
196
  const id = workspaceId.trim();
@@ -148,12 +198,139 @@ function getWindowByWorkspaceId(workspaceId) {
148
198
  !win.webContents.isDestroyed() &&
149
199
  windowWorkspaces.get(win.webContents.id) === id)) !== null && _a !== void 0 ? _a : null;
150
200
  }
151
- function callAgentRendererApi(workspaceId, method, payload) {
201
+ function removeWorkspaceSnapshot(workspaceId) {
202
+ try {
203
+ const configPath = path.join(electron_1.app.getPath('userData'), 'config.json');
204
+ if (!fs.existsSync(configPath))
205
+ return;
206
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
207
+ const current = Array.isArray(config.workspaceSnapshots) ? config.workspaceSnapshots : [];
208
+ const next = current.filter((item) => (item === null || item === void 0 ? void 0 : item.id) !== workspaceId);
209
+ if (next.length !== current.length) {
210
+ fs.writeFileSync(configPath, JSON.stringify(Object.assign(Object.assign({}, config), { workspaceSnapshots: next })));
211
+ }
212
+ }
213
+ catch (_a) {
214
+ // ignore snapshot cleanup errors
215
+ }
216
+ }
217
+ function clearAllWorkspaceSnapshots() {
218
+ try {
219
+ const configPath = path.join(electron_1.app.getPath('userData'), 'config.json');
220
+ if (!fs.existsSync(configPath))
221
+ return;
222
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
223
+ const nextConfig = Object.assign(Object.assign({}, config), { workspaceSnapshots: [] });
224
+ fs.writeFileSync(configPath, JSON.stringify(nextConfig));
225
+ broadcastConfigUpdated(nextConfig);
226
+ }
227
+ catch (_a) {
228
+ // ignore snapshot cleanup errors
229
+ }
230
+ }
231
+ function pickWindowToKeep(allWindows) {
232
+ var _a, _b;
233
+ if (allWindows.length === 0)
234
+ return null;
235
+ return ((_b = (_a = electron_1.BrowserWindow.getFocusedWindow()) !== null && _a !== void 0 ? _a : (mainWindow && !mainWindow.isDestroyed() ? mainWindow : null)) !== null && _b !== void 0 ? _b : allWindows[0]);
236
+ }
237
+ function resetWindowToEmptySession(win) {
238
+ const webContentsId = win.webContents.id;
239
+ const oldWorkspaceId = windowWorkspaces.get(webContentsId);
240
+ if (oldWorkspaceId) {
241
+ agentControlledSessions.delete(oldWorkspaceId);
242
+ closedWorkspaceIds.add(oldWorkspaceId);
243
+ removeWorkspaceSnapshot(oldWorkspaceId);
244
+ }
245
+ cleanupWindowFolder(webContentsId);
246
+ const newWorkspaceId = createWorkspaceId();
247
+ closedWorkspaceIds.delete(newWorkspaceId);
248
+ windowWorkspaces.set(webContentsId, newWorkspaceId);
249
+ windowTitles.set(webContentsId, 'Новый оффер');
250
+ syncWindowTitleForAgentControl(win, newWorkspaceId);
251
+ mainWindow = win;
252
+ win.loadFile(path.join(__dirname, 'index.html'), {
253
+ query: {
254
+ workspaceId: newWorkspaceId,
255
+ agentControlled: '0',
256
+ },
257
+ });
258
+ }
259
+ function closeAllWorkspaceSessions() {
260
+ const allWindows = electron_1.BrowserWindow.getAllWindows().filter(win => !win.isDestroyed());
261
+ const keepWin = pickWindowToKeep(allWindows);
262
+ if (!keepWin) {
263
+ createWindow();
264
+ return { ok: true };
265
+ }
266
+ const keepId = keepWin.webContents.id;
267
+ clearAllWorkspaceSnapshots();
268
+ for (const win of allWindows) {
269
+ if (win.webContents.id === keepId)
270
+ continue;
271
+ const workspaceId = windowWorkspaces.get(win.webContents.id);
272
+ if (workspaceId) {
273
+ closeSessionByWorkspaceId(workspaceId);
274
+ continue;
275
+ }
276
+ const webContentsId = win.webContents.id;
277
+ closingWindowIds.add(webContentsId);
278
+ broadcastWorkspaceWindowsUpdated();
279
+ win.destroy();
280
+ setImmediate(() => broadcastWorkspaceWindowsUpdated());
281
+ }
282
+ resetWindowToEmptySession(keepWin);
283
+ broadcastWorkspaceWindowsUpdated();
284
+ return { ok: true };
285
+ }
286
+ function closeOtherWorkspaceSessions(exceptWebContentsId) {
287
+ const keepId = Number(exceptWebContentsId);
288
+ const allWindows = electron_1.BrowserWindow.getAllWindows().filter(win => !win.isDestroyed());
289
+ for (const win of allWindows) {
290
+ const webContentsId = win.webContents.id;
291
+ if (webContentsId === keepId)
292
+ continue;
293
+ const workspaceId = windowWorkspaces.get(webContentsId);
294
+ if (workspaceId) {
295
+ closeSessionByWorkspaceId(workspaceId);
296
+ }
297
+ else {
298
+ closingWindowIds.add(webContentsId);
299
+ broadcastWorkspaceWindowsUpdated();
300
+ win.destroy();
301
+ setImmediate(() => broadcastWorkspaceWindowsUpdated());
302
+ }
303
+ }
304
+ return { ok: true };
305
+ }
306
+ function closeSessionByWorkspaceId(workspaceId) {
307
+ const id = workspaceId.trim();
308
+ if (!id) {
309
+ return { ok: false, error: 'Session id is required.' };
310
+ }
311
+ const win = getWindowByWorkspaceId(id);
312
+ if (!win || win.isDestroyed()) {
313
+ return { ok: false, error: 'Session not found.' };
314
+ }
315
+ const webContentsId = win.webContents.id;
316
+ agentControlledSessions.delete(id);
317
+ closedWorkspaceIds.add(id);
318
+ removeWorkspaceSnapshot(id);
319
+ closingWindowIds.add(webContentsId);
320
+ broadcastWorkspaceWindowsUpdated();
321
+ win.destroy();
322
+ setImmediate(() => broadcastWorkspaceWindowsUpdated());
323
+ return { ok: true };
324
+ }
325
+ function callAgentRendererApi(workspaceId, method, payload, options) {
152
326
  return __awaiter(this, void 0, void 0, function* () {
153
327
  const win = getWindowByWorkspaceId(workspaceId);
154
328
  if (!win) {
155
329
  return { ok: false, status: 404, error: 'Session not found.' };
156
330
  }
331
+ if ((0, agentControlClaim_1.shouldClaimAgentControl)(method, options)) {
332
+ setSessionAgentControlMode(workspaceId, 'agent');
333
+ }
157
334
  try {
158
335
  const script = `
159
336
  (async () => {
@@ -190,9 +367,10 @@ function callAgentRendererApi(workspaceId, method, payload) {
190
367
  }
191
368
  function callFirstReadyAgentRendererApi(method, payload, preferredSessionId) {
192
369
  return __awaiter(this, void 0, void 0, function* () {
370
+ const readOnlyOptions = { claimAgentControl: false };
193
371
  const preferredId = String(preferredSessionId !== null && preferredSessionId !== void 0 ? preferredSessionId : '').trim();
194
372
  if (preferredId) {
195
- const preferred = yield callAgentRendererApi(preferredId, method, payload);
373
+ const preferred = yield callAgentRendererApi(preferredId, method, payload, readOnlyOptions);
196
374
  if (preferred.ok)
197
375
  return preferred;
198
376
  }
@@ -200,7 +378,7 @@ function callFirstReadyAgentRendererApi(method, payload, preferredSessionId) {
200
378
  if (focused && !focused.isDestroyed() && !focused.webContents.isDestroyed()) {
201
379
  const focusedId = windowWorkspaces.get(focused.webContents.id);
202
380
  if (focusedId) {
203
- const focusedResponse = yield callAgentRendererApi(focusedId, method, payload);
381
+ const focusedResponse = yield callAgentRendererApi(focusedId, method, payload, readOnlyOptions);
204
382
  if (focusedResponse.ok)
205
383
  return focusedResponse;
206
384
  }
@@ -213,7 +391,7 @@ function callFirstReadyAgentRendererApi(method, payload, preferredSessionId) {
213
391
  const sessionId = windowWorkspaces.get(win.webContents.id);
214
392
  if (!sessionId)
215
393
  continue;
216
- const response = yield callAgentRendererApi(sessionId, method, payload);
394
+ const response = yield callAgentRendererApi(sessionId, method, payload, readOnlyOptions);
217
395
  if (response.ok)
218
396
  return response;
219
397
  }
@@ -236,1158 +414,29 @@ function waitForAgentSessionReady(sessionId_1) {
236
414
  return null;
237
415
  });
238
416
  }
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
417
  function startAgentApiServer() {
1106
418
  if (agentApiServer)
1107
419
  return;
1108
420
  const api = (0, express_1.default)();
1109
421
  api.disable('x-powered-by');
1110
422
  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));
423
+ (0, routes_1.registerAgentApiRoutes)(api, {
424
+ host: AGENT_API_HOST,
425
+ port: AGENT_API_PORT,
426
+ version: electron_1.app.getVersion(),
427
+ getAgentWorkSessions,
428
+ isAgentApiReadyInSession,
429
+ agentControlledSessionCount: () => agentControlledSessions.size,
430
+ createWorkspaceId,
431
+ createWindow,
432
+ broadcastWorkspaceWindowsUpdated,
433
+ waitForAgentSessionReady,
434
+ getAgentApiBaseUrl,
435
+ callFirstReadyAgentRendererApi,
436
+ callAgentRendererApi,
437
+ getWindowByWorkspaceId,
438
+ setSessionAgentControlMode,
439
+ closeSession: closeSessionByWorkspaceId,
1391
440
  });
1392
441
  agentApiServer = api.listen(AGENT_API_PORT, AGENT_API_HOST, () => {
1393
442
  console.log(`[Agent API] Listening on http://${AGENT_API_HOST}:${AGENT_API_PORT}`);
@@ -1420,7 +469,27 @@ function cleanupWindowFolder(webContentsId) {
1420
469
  if (!folderId)
1421
470
  return;
1422
471
  windowFolders.delete(webContentsId);
1423
- if (folderOwners.get(folderId) === webContentsId) {
472
+ const owners = folderOwners.get(folderId);
473
+ if (!owners)
474
+ return;
475
+ owners.delete(webContentsId);
476
+ if (owners.size === 0) {
477
+ folderOwners.delete(folderId);
478
+ }
479
+ }
480
+ function pruneStaleFolderOwners(folderId) {
481
+ const owners = folderOwners.get(folderId);
482
+ if (!owners)
483
+ return;
484
+ for (const ownerId of [...owners]) {
485
+ const ownerWin = electron_1.BrowserWindow.getAllWindows().find(w => w.webContents.id === ownerId);
486
+ const ownerAlive = Boolean(ownerWin) && !ownerWin.isDestroyed() && !ownerWin.webContents.isDestroyed();
487
+ if (!ownerAlive) {
488
+ owners.delete(ownerId);
489
+ windowFolders.delete(ownerId);
490
+ }
491
+ }
492
+ if (owners.size === 0) {
1424
493
  folderOwners.delete(folderId);
1425
494
  }
1426
495
  }
@@ -1457,6 +526,8 @@ function telegramSendMessageViaBot(botToken, chatId, text) {
1457
526
  return __awaiter(this, void 0, void 0, function* () {
1458
527
  const apiUrl = `https://api.telegram.org/bot${encodeURIComponent(botToken)}/sendMessage`;
1459
528
  const safeText = text.length > 4096 ? `${text.slice(0, 4090)}…` : text;
529
+ const controller = new AbortController();
530
+ const timeoutId = setTimeout(() => controller.abort(), 30000);
1460
531
  try {
1461
532
  const res = yield fetch(apiUrl, {
1462
533
  method: 'POST',
@@ -1465,6 +536,7 @@ function telegramSendMessageViaBot(botToken, chatId, text) {
1465
536
  chat_id: chatId,
1466
537
  text: safeText,
1467
538
  }),
539
+ signal: controller.signal,
1468
540
  });
1469
541
  const data = (yield res.json());
1470
542
  if (!data.ok) {
@@ -1481,6 +553,9 @@ function telegramSendMessageViaBot(botToken, chatId, text) {
1481
553
  error: e instanceof Error ? e.message : String(e),
1482
554
  };
1483
555
  }
556
+ finally {
557
+ clearTimeout(timeoutId);
558
+ }
1484
559
  });
1485
560
  }
1486
561
  function abortPendingGoogleAuth(reason) {
@@ -1562,7 +637,11 @@ function buildAppMenu() {
1562
637
  });
1563
638
  electron_1.Menu.setApplicationMenu(electron_1.Menu.buildFromTemplate(template));
1564
639
  }
1565
- function createWindow(workspaceId = createWorkspaceId()) {
640
+ function createWindow(workspaceId = createWorkspaceId(), options) {
641
+ closedWorkspaceIds.delete(workspaceId);
642
+ if (options === null || options === void 0 ? void 0 : options.agentControlled) {
643
+ agentControlledSessions.add(workspaceId);
644
+ }
1566
645
  const win = new electron_1.BrowserWindow({
1567
646
  width: 1000,
1568
647
  height: 800,
@@ -1584,6 +663,10 @@ function createWindow(workspaceId = createWorkspaceId()) {
1584
663
  win.on('closed', () => {
1585
664
  var _a;
1586
665
  closingWindowIds.delete(win.webContents.id);
666
+ const closedWorkspaceId = windowWorkspaces.get(win.webContents.id);
667
+ if (closedWorkspaceId) {
668
+ agentControlledSessions.delete(closedWorkspaceId);
669
+ }
1587
670
  windows.delete(win);
1588
671
  cleanupWindowFolder(win.webContents.id);
1589
672
  windowWorkspaces.delete(win.webContents.id);
@@ -1652,7 +735,10 @@ function createWindow(workspaceId = createWorkspaceId()) {
1652
735
  });
1653
736
  // We need to point to the webpack output html
1654
737
  win.loadFile(path.join(__dirname, 'index.html'), {
1655
- query: { workspaceId },
738
+ query: {
739
+ workspaceId,
740
+ agentControlled: (options === null || options === void 0 ? void 0 : options.agentControlled) ? '1' : '0',
741
+ },
1656
742
  });
1657
743
  // DevTools disabled by default as requested
1658
744
  // win.webContents.openDevTools();
@@ -1693,8 +779,12 @@ if (gotSingleInstanceLock)
1693
779
  };
1694
780
  const writeConfig = (config) => {
1695
781
  try {
1696
- fs.writeFileSync(configPath, JSON.stringify(config));
1697
- broadcastConfigUpdated(config);
782
+ const sanitizedConfig = Object.assign({}, config);
783
+ if (Array.isArray(sanitizedConfig.workspaceSnapshots) && closedWorkspaceIds.size > 0) {
784
+ sanitizedConfig.workspaceSnapshots = sanitizedConfig.workspaceSnapshots.filter((item) => { var _a; return !closedWorkspaceIds.has(String((_a = item === null || item === void 0 ? void 0 : item.id) !== null && _a !== void 0 ? _a : '')); });
785
+ }
786
+ fs.writeFileSync(configPath, JSON.stringify(sanitizedConfig));
787
+ broadcastConfigUpdated(sanitizedConfig);
1698
788
  return true;
1699
789
  }
1700
790
  catch (_a) {
@@ -1725,6 +815,8 @@ if (gotSingleInstanceLock)
1725
815
  const id = String((_a = snapshot === null || snapshot === void 0 ? void 0 : snapshot.id) !== null && _a !== void 0 ? _a : '').trim();
1726
816
  if (!id)
1727
817
  return { ok: false };
818
+ if (closedWorkspaceIds.has(id))
819
+ return { ok: false, error: 'Workspace is closed.' };
1728
820
  const config = readConfig();
1729
821
  const current = Array.isArray(config.workspaceSnapshots) ? config.workspaceSnapshots : [];
1730
822
  const nextSnapshot = Object.assign(Object.assign({}, snapshot), { id, updatedAt: typeof snapshot.updatedAt === 'string' ? snapshot.updatedAt : new Date().toISOString() });
@@ -1742,17 +834,14 @@ if (gotSingleInstanceLock)
1742
834
  cleanupWindowFolder(webContentsId);
1743
835
  return { ok: true };
1744
836
  }
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
- }
837
+ pruneStaleFolderOwners(folderId);
1754
838
  cleanupWindowFolder(webContentsId);
1755
- folderOwners.set(folderId, webContentsId);
839
+ let owners = folderOwners.get(folderId);
840
+ if (!owners) {
841
+ owners = new Set();
842
+ folderOwners.set(folderId, owners);
843
+ }
844
+ owners.add(webContentsId);
1756
845
  windowFolders.set(webContentsId, folderId);
1757
846
  broadcastWorkspaceWindowsUpdated();
1758
847
  return { ok: true };
@@ -1771,21 +860,22 @@ if (gotSingleInstanceLock)
1771
860
  return { ok: true };
1772
861
  }));
1773
862
  electron_1.ipcMain.handle('set-workspace-window-meta', (event, payload) => __awaiter(void 0, void 0, void 0, function* () {
1774
- var _a, _b, _c;
863
+ var _a, _b, _c, _d;
1775
864
  const webContentsId = event.sender.id;
1776
865
  const title = String((_a = payload === null || payload === void 0 ? void 0 : payload.title) !== null && _a !== void 0 ? _a : '').trim();
1777
866
  if (title) {
1778
867
  windowTitles.set(webContentsId, title);
1779
868
  const win = electron_1.BrowserWindow.fromWebContents(event.sender);
1780
- if (win && !win.isDestroyed()) {
1781
- win.setTitle(`${APP_WINDOW_TITLE} ${title}`);
869
+ const workspaceId = (_b = windowWorkspaces.get(webContentsId)) !== null && _b !== void 0 ? _b : '';
870
+ if (win && !win.isDestroyed() && workspaceId) {
871
+ syncWindowTitleForAgentControl(win, workspaceId);
1782
872
  }
1783
873
  }
1784
- const folderId = String((_b = payload === null || payload === void 0 ? void 0 : payload.folderId) !== null && _b !== void 0 ? _b : '').trim();
874
+ const folderId = String((_c = payload === null || payload === void 0 ? void 0 : payload.folderId) !== null && _c !== void 0 ? _c : '').trim();
1785
875
  if (folderId) {
1786
876
  windowFolders.set(webContentsId, folderId);
1787
877
  }
1788
- const folderPath = String((_c = payload === null || payload === void 0 ? void 0 : payload.folderPath) !== null && _c !== void 0 ? _c : '').trim();
878
+ const folderPath = String((_d = payload === null || payload === void 0 ? void 0 : payload.folderPath) !== null && _d !== void 0 ? _d : '').trim();
1789
879
  if (folderPath) {
1790
880
  windowFolderPaths.set(webContentsId, folderPath);
1791
881
  }
@@ -1807,19 +897,31 @@ if (gotSingleInstanceLock)
1807
897
  if (!win || win.isDestroyed())
1808
898
  return { ok: false };
1809
899
  const workspaceId = windowWorkspaces.get(id);
900
+ if (!workspaceId) {
901
+ closingWindowIds.add(id);
902
+ broadcastWorkspaceWindowsUpdated();
903
+ win.destroy();
904
+ setImmediate(() => broadcastWorkspaceWindowsUpdated());
905
+ return { ok: true };
906
+ }
907
+ return closeSessionByWorkspaceId(workspaceId);
908
+ }));
909
+ electron_1.ipcMain.handle('close-all-workspace-windows', () => __awaiter(void 0, void 0, void 0, function* () { return closeAllWorkspaceSessions(); }));
910
+ electron_1.ipcMain.handle('close-other-workspace-windows', (event) => __awaiter(void 0, void 0, void 0, function* () { return closeOtherWorkspaceSessions(event.sender.id); }));
911
+ electron_1.ipcMain.handle('get-agent-control-mode', (event) => __awaiter(void 0, void 0, void 0, function* () {
912
+ var _a;
913
+ const workspaceId = (_a = windowWorkspaces.get(event.sender.id)) !== null && _a !== void 0 ? _a : '';
914
+ return {
915
+ mode: workspaceId && agentControlledSessions.has(workspaceId) ? 'agent' : 'manual',
916
+ };
917
+ }));
918
+ electron_1.ipcMain.handle('release-agent-control', (event) => __awaiter(void 0, void 0, void 0, function* () {
919
+ var _a;
920
+ const workspaceId = (_a = windowWorkspaces.get(event.sender.id)) !== null && _a !== void 0 ? _a : '';
1810
921
  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
- }
922
+ setSessionAgentControlMode(workspaceId, 'manual');
1817
923
  }
1818
- closingWindowIds.add(id);
1819
- broadcastWorkspaceWindowsUpdated();
1820
- win.destroy();
1821
- setImmediate(() => broadcastWorkspaceWindowsUpdated());
1822
- return { ok: true };
924
+ return { ok: true, mode: 'manual' };
1823
925
  }));
1824
926
  electron_1.ipcMain.handle('telegram-send-test', (_event, payload) => __awaiter(void 0, void 0, void 0, function* () {
1825
927
  var _a, _b;