@sequent-org/moodboard 1.4.8 → 1.4.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sequent-org/moodboard",
3
- "version": "1.4.8",
3
+ "version": "1.4.10",
4
4
  "type": "module",
5
5
  "description": "Interactive moodboard",
6
6
  "main": "./src/index.js",
@@ -1,5 +1,3 @@
1
- import { isV2ImageDownloadUrl } from '../services/AssetUrlPolicy.js';
2
-
3
1
  // src/core/ApiClient.js
4
2
  export class ApiClient {
5
3
  constructor(baseUrl, authToken = null) {
@@ -66,6 +64,14 @@ export class ApiClient {
66
64
 
67
65
  // Фильтруем объекты изображений и файлов - убираем избыточные данные
68
66
  const cleanedData = this._cleanObjectData(payloadBoardData);
67
+ const objects = Array.isArray(cleanedData?.objects) ? cleanedData.objects : [];
68
+ const imageObjects = objects.filter((obj) => obj?.type === 'image');
69
+ const imageObjectsWithSrc = imageObjects.filter((obj) => typeof obj?.src === 'string' && obj.src.trim().length > 0);
70
+ console.log('history/save payload stats:', {
71
+ totalObjects: objects.length,
72
+ imageObjects: imageObjects.length,
73
+ imageObjectsWithSrc: imageObjectsWithSrc.length
74
+ });
69
75
 
70
76
  const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
71
77
 
@@ -146,39 +152,28 @@ export class ApiClient {
146
152
 
147
153
  const cleanedObjects = boardData.objects.map(obj => {
148
154
  if (obj.type === 'image') {
149
- const imageId = typeof obj.imageId === 'string' ? obj.imageId.trim() : '';
150
- const topSrc = typeof obj.src === 'string' ? obj.src : '';
151
- const propSrc = typeof obj.properties?.src === 'string' ? obj.properties.src : '';
152
- const hasForbiddenInlineSrc = /^data:/i.test(topSrc)
153
- || /^blob:/i.test(topSrc)
154
- || /^data:/i.test(propSrc)
155
- || /^blob:/i.test(propSrc);
155
+ const topSrcRaw = typeof obj.src === 'string' ? obj.src : '';
156
+ const propSrcRaw = typeof obj.properties?.src === 'string' ? obj.properties.src : '';
157
+ const normalizedSrc = topSrcRaw.trim() || propSrcRaw.trim();
156
158
 
157
- // Жесткий контракт v2: сохраняем image только через server imageId.
158
- if (!imageId) {
159
- throw new Error(`Image object "${obj.id || 'unknown'}" has no imageId. Save is blocked.`);
159
+ if (!normalizedSrc) {
160
+ throw new Error(`Image object "${obj.id || 'unknown'}" has no src. Save is blocked.`);
160
161
  }
161
- if (hasForbiddenInlineSrc) {
162
+ if (/^data:/i.test(normalizedSrc) || /^blob:/i.test(normalizedSrc)) {
162
163
  throw new Error(`Image object "${obj.id || 'unknown'}" contains forbidden data/blob src. Save is blocked.`);
163
164
  }
164
- if (topSrc && !isV2ImageDownloadUrl(topSrc)) {
165
- throw new Error(`Image object "${obj.id || 'unknown'}" has non-v2 src URL. Save is blocked.`);
166
- }
167
- if (propSrc && !isV2ImageDownloadUrl(propSrc)) {
168
- throw new Error(`Image object "${obj.id || 'unknown'}" has non-v2 properties.src URL. Save is blocked.`);
165
+ if (/^\/api\/images\//i.test(normalizedSrc)) {
166
+ throw new Error(`Image object "${obj.id || 'unknown'}" has legacy src URL. Save is blocked.`);
169
167
  }
170
168
 
171
- const cleanedObj = { ...obj };
172
-
173
- // imageId валиден — src можно безопасно убрать из history payload.
174
- if (cleanedObj.src) {
175
- delete cleanedObj.src;
176
- }
169
+ const cleanedObj = {
170
+ ...obj,
171
+ src: normalizedSrc
172
+ };
177
173
  if (cleanedObj.properties?.src) {
178
174
  cleanedObj.properties = { ...cleanedObj.properties };
179
175
  delete cleanedObj.properties.src;
180
176
  }
181
-
182
177
  return cleanedObj;
183
178
  }
184
179
 
@@ -216,7 +211,7 @@ export class ApiClient {
216
211
  }
217
212
 
218
213
  /**
219
- * Восстанавливает URL изображений и файлов при загрузке
214
+ * Нормализует URL ресурсов при загрузке
220
215
  */
221
216
  async restoreObjectUrls(boardData) {
222
217
  if (!boardData || !boardData.objects) {
@@ -226,25 +221,19 @@ export class ApiClient {
226
221
  const restoredObjects = await Promise.all(
227
222
  boardData.objects.map(async (obj) => {
228
223
  if (obj.type === 'image') {
229
- if (obj.imageId && (!obj.src && !obj.properties?.src)) {
230
- try {
231
- // Формируем URL изображения
232
- const imageUrl = `/api/v2/images/${obj.imageId}/download`;
233
-
234
- return {
235
- ...obj,
236
- src: imageUrl,
237
- properties: {
238
- ...obj.properties,
239
- src: imageUrl
240
- }
241
- };
242
- } catch (error) {
243
- console.warn(`Не удалось восстановить URL для изображения ${obj.imageId}:`, error);
244
- return obj;
245
- }
224
+ const topSrc = typeof obj.src === 'string' ? obj.src.trim() : '';
225
+ const propSrc = typeof obj.properties?.src === 'string' ? obj.properties.src.trim() : '';
226
+ const normalizedSrc = topSrc || propSrc;
227
+ if (!normalizedSrc) return obj;
228
+ const restoredObj = {
229
+ ...obj,
230
+ src: normalizedSrc
231
+ };
232
+ if (restoredObj.properties?.src) {
233
+ restoredObj.properties = { ...restoredObj.properties };
234
+ delete restoredObj.properties.src;
246
235
  }
247
- return obj;
236
+ return restoredObj;
248
237
  }
249
238
 
250
239
  if (obj.type === 'file') {
@@ -3,7 +3,6 @@
3
3
  */
4
4
  import { Events } from './events/Events.js';
5
5
  import { logMindmapCompoundDebug } from '../mindmap/MindmapCompoundContract.js';
6
- import { isV2ImageDownloadUrl } from '../services/AssetUrlPolicy.js';
7
6
  export class SaveManager {
8
7
  constructor(eventBus, options = {}) {
9
8
  this.eventBus = eventBus;
@@ -106,7 +105,7 @@ export class SaveManager {
106
105
  }
107
106
 
108
107
  // Жесткий контракт для сохранения картинок:
109
- // - каждый image обязан иметь imageId
108
+ // - каждый image обязан иметь src
110
109
  // - data:/blob: URL в image запрещены
111
110
  this._assertImageSaveContract(saveData);
112
111
 
@@ -236,21 +235,18 @@ export class SaveManager {
236
235
 
237
236
  for (const obj of objects) {
238
237
  if (!obj || obj.type !== 'image') continue;
239
- const imageId = typeof obj.imageId === 'string' ? obj.imageId.trim() : '';
240
238
  const topSrc = typeof obj.src === 'string' ? obj.src : '';
241
239
  const propSrc = typeof obj.properties?.src === 'string' ? obj.properties.src : '';
240
+ const effectiveSrc = topSrc.trim() || propSrc.trim();
242
241
 
243
- if (!imageId) {
244
- throw new Error(`Image object "${obj.id || 'unknown'}" has no imageId. Save is blocked.`);
242
+ if (!effectiveSrc) {
243
+ throw new Error(`Image object "${obj.id || 'unknown'}" has no src. Save is blocked.`);
245
244
  }
246
245
  if (/^data:/i.test(topSrc) || /^blob:/i.test(topSrc) || /^data:/i.test(propSrc) || /^blob:/i.test(propSrc)) {
247
246
  throw new Error(`Image object "${obj.id || 'unknown'}" contains forbidden data/blob src. Save is blocked.`);
248
247
  }
249
- if (topSrc && !isV2ImageDownloadUrl(topSrc)) {
250
- throw new Error(`Image object "${obj.id || 'unknown'}" has non-v2 src URL. Save is blocked.`);
251
- }
252
- if (propSrc && !isV2ImageDownloadUrl(propSrc)) {
253
- throw new Error(`Image object "${obj.id || 'unknown'}" has non-v2 properties.src URL. Save is blocked.`);
248
+ if (/^\/api\/images\//i.test(effectiveSrc)) {
249
+ throw new Error(`Image object "${obj.id || 'unknown'}" has legacy src URL. Save is blocked.`);
254
250
  }
255
251
  }
256
252
  }
@@ -1,7 +1,6 @@
1
1
  import { Events } from '../events/Events.js';
2
2
  import { PasteObjectCommand } from '../commands/index.js';
3
3
  import { RevitScreenshotMetadataService } from '../../services/RevitScreenshotMetadataService.js';
4
- import { isV2ImageDownloadUrl } from '../../services/AssetUrlPolicy.js';
5
4
 
6
5
  export function setupClipboardFlow(core) {
7
6
  const revitMetadataService = new RevitScreenshotMetadataService(console);
@@ -20,14 +19,10 @@ export function setupClipboardFlow(core) {
20
19
  };
21
20
  };
22
21
 
23
- const ensureServerImage = async ({ src, name, imageId }) => {
24
- if (imageId) {
25
- const serverUrl = typeof src === 'string' ? src.trim() : '';
26
- if (!isV2ImageDownloadUrl(serverUrl)) {
27
- alert('Некорректный адрес изображения. Изображение не добавлено.');
28
- return null;
29
- }
30
- return { src: serverUrl, name, imageId };
22
+ const ensureServerImage = async ({ src, name }) => {
23
+ const srcValue = typeof src === 'string' ? src.trim() : '';
24
+ if (srcValue && !/^data:image\//i.test(srcValue) && !/^blob:/i.test(srcValue)) {
25
+ return { src: srcValue, name };
31
26
  }
32
27
  if (!core.imageUploadService) {
33
28
  alert('Сервис загрузки изображений недоступен. Изображение не добавлено.');
@@ -46,13 +41,12 @@ export function setupClipboardFlow(core) {
46
41
  uploadResult = await core.imageUploadService.uploadImage(blob, name || 'clipboard-image');
47
42
  }
48
43
  const serverUrl = typeof uploadResult.url === 'string' ? uploadResult.url.trim() : '';
49
- if (!isV2ImageDownloadUrl(serverUrl)) {
44
+ if (!serverUrl || /^data:/i.test(serverUrl) || /^blob:/i.test(serverUrl)) {
50
45
  throw new Error('Сервер вернул некорректный URL изображения');
51
46
  }
52
47
  return {
53
48
  src: serverUrl,
54
- name: uploadResult.name || name,
55
- imageId: uploadResult.imageId || uploadResult.id
49
+ name: uploadResult.name || name
56
50
  };
57
51
  } catch (error) {
58
52
  console.error('Ошибка загрузки вставленного изображения на сервер:', error);
@@ -228,10 +222,10 @@ export function setupClipboardFlow(core) {
228
222
  core._cursor.y = y;
229
223
  });
230
224
 
231
- core.eventBus.on(Events.UI.PasteImage, async ({ src, name, imageId }) => {
225
+ core.eventBus.on(Events.UI.PasteImage, async ({ src, name }) => {
232
226
  if (!src) return;
233
- const uploaded = await ensureServerImage({ src, name, imageId });
234
- if (!uploaded?.imageId) return;
227
+ const uploaded = await ensureServerImage({ src, name });
228
+ if (!uploaded?.src) return;
235
229
  const view = core.pixi.app.view;
236
230
  const world = core.pixi.worldLayer || core.pixi.app.stage;
237
231
  const s = world?.scale?.x || 1;
@@ -269,12 +263,10 @@ export function setupClipboardFlow(core) {
269
263
  height: h,
270
264
  ...revitPayload.properties
271
265
  };
272
- const extraData = uploaded.imageId ? { imageId: uploaded.imageId } : {};
273
266
  core.createObject(
274
267
  revitPayload.type,
275
268
  { x: Math.round(worldX - Math.round(w / 2)), y: Math.round(worldY - Math.round(h / 2)) },
276
- properties,
277
- extraData
269
+ properties
278
270
  );
279
271
  };
280
272
 
@@ -291,10 +283,10 @@ export function setupClipboardFlow(core) {
291
283
  }
292
284
  });
293
285
 
294
- core.eventBus.on(Events.UI.PasteImageAt, async ({ x, y, src, name, imageId }) => {
286
+ core.eventBus.on(Events.UI.PasteImageAt, async ({ x, y, src, name }) => {
295
287
  if (!src) return;
296
- const uploaded = await ensureServerImage({ src, name, imageId });
297
- if (!uploaded?.imageId) return;
288
+ const uploaded = await ensureServerImage({ src, name });
289
+ if (!uploaded?.src) return;
298
290
  const world = core.pixi.worldLayer || core.pixi.app.stage;
299
291
  const s = world?.scale?.x || 1;
300
292
  const worldX = (x - (world?.x || 0)) / s;
@@ -319,12 +311,10 @@ export function setupClipboardFlow(core) {
319
311
  height: h,
320
312
  ...revitPayload.properties
321
313
  };
322
- const extraData = uploaded.imageId ? { imageId: uploaded.imageId } : {};
323
314
  core.createObject(
324
315
  revitPayload.type,
325
316
  { x: Math.round(worldX - Math.round(w / 2)), y: Math.round(worldY - Math.round(h / 2)) },
326
- properties,
327
- extraData
317
+ properties
328
318
  );
329
319
  };
330
320
 
package/src/core/index.js CHANGED
@@ -416,8 +416,15 @@ export class CoreMoodBoard {
416
416
  transform: {
417
417
  pivotCompensated: false // Новые объекты еще не скомпенсированы
418
418
  },
419
- ...extraData // Добавляем дополнительные данные (например, imageId)
419
+ ...extraData
420
420
  };
421
+ if ((type === 'image' || type === 'revit-screenshot-img') && typeof properties?.src === 'string') {
422
+ objectData.src = properties.src;
423
+ if (objectData.properties && objectData.properties.src) {
424
+ objectData.properties = { ...objectData.properties };
425
+ delete objectData.properties.src;
426
+ }
427
+ }
421
428
  objectData.properties = normalizeMindmapPropertiesForCreate({
422
429
  type,
423
430
  objectId: objectData.id,
@@ -517,6 +524,18 @@ export class CoreMoodBoard {
517
524
  properties: objectData.properties || {},
518
525
  existingObjects: this.state?.state?.objects || [],
519
526
  });
527
+ if (objectData.type === 'image' || objectData.type === 'revit-screenshot-img') {
528
+ const topSrc = typeof objectData.src === 'string' ? objectData.src.trim() : '';
529
+ const propSrc = typeof objectData.properties?.src === 'string' ? objectData.properties.src.trim() : '';
530
+ const normalizedSrc = topSrc || propSrc;
531
+ if (normalizedSrc) {
532
+ objectData.src = normalizedSrc;
533
+ }
534
+ if (objectData.properties?.src) {
535
+ objectData.properties = { ...objectData.properties };
536
+ delete objectData.properties.src;
537
+ }
538
+ }
520
539
  if (objectData.type === 'mindmap') {
521
540
  logMindmapCompoundDebug('core:load-object', {
522
541
  id: objectData.id,
@@ -628,45 +647,6 @@ export class CoreMoodBoard {
628
647
  return this.state.getObjects().find(o => o.id === objectId);
629
648
  }
630
649
 
631
- /**
632
- * Очищает неиспользуемые изображения с сервера
633
- * @returns {Promise<{deletedCount: number, errors: Array}>}
634
- */
635
- async cleanupUnusedImages() {
636
- try {
637
- if (!this.imageUploadService) {
638
- console.warn('ImageUploadService недоступен для очистки изображений');
639
- return { deletedCount: 0, errors: ['ImageUploadService недоступен'] };
640
- }
641
-
642
- const result = await this.imageUploadService.cleanupUnusedImages();
643
-
644
- // Проверяем результат на корректность
645
- if (!result || typeof result !== 'object') {
646
- console.warn('Некорректный ответ от ImageUploadService:', result);
647
- return { deletedCount: 0, errors: ['Некорректный ответ сервера'] };
648
- }
649
-
650
- const deletedCount = Number(result.deletedCount) || 0;
651
- const errors = Array.isArray(result.errors) ? result.errors : [];
652
-
653
- if (deletedCount > 0) {
654
- console.log(`Очищено ${deletedCount} неиспользуемых изображений`);
655
- }
656
- if (errors.length > 0) {
657
- console.warn('Ошибки при очистке изображений:', errors);
658
- }
659
-
660
- return { deletedCount, errors };
661
- } catch (error) {
662
- console.error('Ошибка при автоматической очистке изображений:', error);
663
- return {
664
- deletedCount: 0,
665
- errors: [error?.message || 'Неизвестная ошибка']
666
- };
667
- }
668
- }
669
-
670
650
  destroy() {
671
651
  // Предотвращаем повторное уничтожение
672
652
  if (this.destroyed) {
@@ -12,8 +12,7 @@ export class KeyboardClipboardImagePaste {
12
12
  const uploadResult = await this.core.imageUploadService.uploadFromDataUrl(dataUrl, fileName);
13
13
  this.eventBus.emit(Events.UI.PasteImage, {
14
14
  src: uploadResult.url,
15
- name: uploadResult.name,
16
- imageId: uploadResult.imageId || uploadResult.id
15
+ name: uploadResult.name
17
16
  });
18
17
  } else {
19
18
  alert('Сервис загрузки изображений недоступен. Изображение не добавлено.');
@@ -30,8 +29,7 @@ export class KeyboardClipboardImagePaste {
30
29
  const uploadResult = await this.core.imageUploadService.uploadImage(file, fileName);
31
30
  this.eventBus.emit(Events.UI.PasteImage, {
32
31
  src: uploadResult.url,
33
- name: uploadResult.name,
34
- imageId: uploadResult.imageId || uploadResult.id
32
+ name: uploadResult.name
35
33
  });
36
34
  } else {
37
35
  alert('Сервис загрузки изображений недоступен. Изображение не добавлено.');
@@ -11,6 +11,8 @@ export class ActionHandler {
11
11
  * Обрабатывает действия тулбара
12
12
  */
13
13
  handleToolbarAction(action) {
14
+ this._assertImageActionContract(action);
15
+
14
16
  switch (action.type) {
15
17
  case 'frame':
16
18
  case 'simple-text':
@@ -24,9 +26,8 @@ export class ActionHandler {
24
26
  case 'comment':
25
27
  case 'file':
26
28
  case 'mindmap':
27
- // Передаем imageId как extraData для изображений, fileId для файлов
28
- const extraData = action.imageId ? { imageId: action.imageId } :
29
- action.fileId ? { fileId: action.fileId } : {};
29
+ // Для изображений используем только src; для файлов сохраняем fileId.
30
+ const extraData = action.fileId ? { fileId: action.fileId } : {};
30
31
  return this.handleCreateObject(action.type, action.position, action.properties || {}, extraData);
31
32
 
32
33
  case 'delete-object':
@@ -113,4 +114,19 @@ export class ActionHandler {
113
114
  exportBoard() {
114
115
  return this.handleExportBoard();
115
116
  }
117
+
118
+ _assertImageActionContract(action) {
119
+ if (!action || (action.type !== 'image' && action.type !== 'revit-screenshot-img')) return;
120
+ const src = typeof action?.properties?.src === 'string' ? action.properties.src.trim() : '';
121
+ if (src) return;
122
+
123
+ const reason = 'в цепочке создания отсутствует обязательное поле properties.src';
124
+ const message = `Загрузить картинку не получилось: ${reason}. Попробуйте еще раз.`;
125
+ if (this.workspaceManager?.showNotification) {
126
+ this.workspaceManager.showNotification(message);
127
+ } else if (typeof alert === 'function') {
128
+ alert(message);
129
+ }
130
+ throw new Error(`Image action contract violated: ${reason}`);
131
+ }
116
132
  }
@@ -86,6 +86,14 @@ export async function loadExistingBoard(board, version = null, options = {}) {
86
86
 
87
87
  if (apiResponse?.success && payload) {
88
88
  const normalizedData = normalizeLoadedPayload(payload, boardId);
89
+ const loadedObjects = Array.isArray(normalizedData.objects) ? normalizedData.objects : [];
90
+ const imageObjects = loadedObjects.filter((obj) => obj?.type === 'image');
91
+ const imageObjectsWithSrc = imageObjects.filter((obj) => typeof obj?.src === 'string' && obj.src.trim().length > 0);
92
+ console.log('MoodBoard load image src diagnostics:', {
93
+ totalObjects: loadedObjects.length,
94
+ imageObjects: imageObjects.length,
95
+ imageObjectsWithSrc: imageObjectsWithSrc.length
96
+ });
89
97
  const loadedVersion = Number(normalizedData.version) || null;
90
98
  board.currentLoadedVersion = loadedVersion;
91
99
  board.historyCursorVersion = loadedVersion;
@@ -6,7 +6,7 @@ import * as PIXI from 'pixi.js';
6
6
  export class ImageObject {
7
7
  constructor(objectData = {}) {
8
8
  this.objectData = objectData;
9
- let src = objectData.properties?.src || objectData.src;
9
+ let src = objectData.src;
10
10
  const isEmojiIcon = !!objectData.properties?.isEmojiIcon;
11
11
  // Не используем устаревшие blob: URL — они недолговечны и приводят к ERR_FILE_NOT_FOUND
12
12
  if (typeof src === 'string' && src.startsWith('blob:')) {
@@ -1,5 +1,3 @@
1
- import { isV2ImageDownloadUrl } from './AssetUrlPolicy.js';
2
-
3
1
  /**
4
2
  * Сервис для загрузки и управления изображениями на сервере
5
3
  */
@@ -7,7 +5,6 @@ export class ImageUploadService {
7
5
  constructor(apiClient, options = {}) {
8
6
  this.apiClient = apiClient;
9
7
  this.uploadEndpoint = '/api/v2/images/upload';
10
- this.deleteEndpoint = '/api/v2/images';
11
8
  this.options = {
12
9
  csrfToken: null, // Можно передать токен напрямую
13
10
  csrfTokenSelector: 'meta[name="csrf-token"]', // Селектор для поиска токена в DOM
@@ -51,7 +48,7 @@ export class ImageUploadService {
51
48
  * Загружает изображение на сервер
52
49
  * @param {File|Blob} file - файл изображения
53
50
  * @param {string} name - имя файла
54
- * @returns {Promise<{id: string, url: string, width: number, height: number}>}
51
+ * @returns {Promise<{url: string, width: number, height: number}>}
55
52
  */
56
53
  async uploadImage(file, name = null) {
57
54
  try {
@@ -99,21 +96,13 @@ export class ImageUploadService {
99
96
  throw new Error(result.message || 'Ошибка загрузки изображения');
100
97
  }
101
98
 
102
- const imageId = result.data.imageId || result.data.id;
103
99
  const serverUrl = typeof result.data.url === 'string' ? result.data.url.trim() : '';
104
- if (!imageId) {
105
- throw new Error('Сервер не вернул imageId.');
106
- }
107
- if (!isV2ImageDownloadUrl(serverUrl)) {
108
- throw new Error('Некорректный URL изображения от сервера. Ожидается /api/v2/images/{imageId}/download');
109
- }
110
- if (!this._matchesImageIdInUrl(serverUrl, imageId)) {
111
- throw new Error('imageId не совпадает с URL изображения от сервера.');
100
+ if (!this._isPersistedImageSrc(serverUrl)) {
101
+ throw new Error('Некорректный URL изображения от сервера. Ожидается непустой src без data:/blob:.');
112
102
  }
103
+ console.log('Image upload response data.url:', serverUrl);
113
104
 
114
105
  return {
115
- id: imageId, // Используем imageId как основное поле, id для обратной совместимости
116
- imageId, // Добавляем imageId для явного доступа
117
106
  url: serverUrl,
118
107
  width: result.data.width,
119
108
  height: result.data.height,
@@ -127,148 +116,26 @@ export class ImageUploadService {
127
116
  }
128
117
  }
129
118
 
130
- _matchesImageIdInUrl(url, imageId) {
131
- const id = typeof imageId === 'string' ? imageId.trim() : '';
132
- if (!id || typeof url !== 'string') return false;
119
+ _isPersistedImageSrc(url) {
120
+ if (typeof url !== 'string') return false;
133
121
  const raw = url.trim();
134
- const relativeMatch = raw.match(/^\/api\/v2\/images\/([^/]+)\/download$/i);
135
- if (relativeMatch) {
136
- return decodeURIComponent(relativeMatch[1]) === id;
137
- }
138
- try {
139
- const parsed = new URL(raw);
140
- const absoluteMatch = parsed.pathname.match(/^\/api\/v2\/images\/([^/]+)\/download$/i);
141
- return !!absoluteMatch && decodeURIComponent(absoluteMatch[1]) === id;
142
- } catch (_) {
143
- return false;
144
- }
122
+ if (!raw) return false;
123
+ if (/^data:/i.test(raw) || /^blob:/i.test(raw)) return false;
124
+ if (/^\/api\/images\//i.test(raw)) return false;
125
+ return true;
145
126
  }
146
127
 
147
128
  /**
148
129
  * Загружает изображение из base64 DataURL
149
130
  * @param {string} dataUrl - base64 DataURL
150
131
  * @param {string} name - имя файла
151
- * @returns {Promise<{id: string, url: string, width: number, height: number}>}
132
+ * @returns {Promise<{url: string, width: number, height: number}>}
152
133
  */
153
134
  async uploadFromDataUrl(dataUrl, name = 'clipboard-image.png') {
154
135
  const blob = await this._dataUrlToBlob(dataUrl);
155
136
  return this.uploadImage(blob, name);
156
137
  }
157
138
 
158
- /**
159
- * Удаляет изображение с сервера
160
- * @param {string} imageId - ID изображения
161
- */
162
- async deleteImage(imageId) {
163
- try {
164
- const csrfToken = this._getCsrfToken();
165
-
166
- const headers = {
167
- 'X-Requested-With': 'XMLHttpRequest',
168
- 'Accept': 'application/json'
169
- };
170
-
171
- // Добавляем CSRF токен только если он есть
172
- if (csrfToken) {
173
- headers['X-CSRF-TOKEN'] = csrfToken;
174
- }
175
-
176
- const response = await fetch(`${this.deleteEndpoint}/${imageId}`, {
177
- method: 'DELETE',
178
- headers,
179
- credentials: 'same-origin'
180
- });
181
-
182
- if (!response.ok) {
183
- const errorData = await response.json().catch(() => null);
184
- throw new Error(errorData?.message || `HTTP ${response.status}: ${response.statusText}`);
185
- }
186
-
187
- const result = await response.json();
188
- return result.success;
189
-
190
- } catch (error) {
191
- console.error('Ошибка удаления изображения:', error);
192
- throw error;
193
- }
194
- }
195
-
196
- /**
197
- * Очищает неиспользуемые изображения с сервера
198
- * @returns {Promise<{deletedCount: number, errors: Array}>}
199
- */
200
- async cleanupUnusedImages() {
201
- try {
202
- const csrfToken = this._getCsrfToken();
203
-
204
- const headers = {
205
- 'X-Requested-With': 'XMLHttpRequest',
206
- 'Accept': 'application/json'
207
- };
208
-
209
- // Добавляем CSRF токен только если он есть
210
- if (csrfToken) {
211
- headers['X-CSRF-TOKEN'] = csrfToken;
212
- }
213
-
214
- const response = await fetch(`${this.deleteEndpoint}/cleanup`, {
215
- method: 'POST',
216
- headers,
217
- credentials: 'same-origin'
218
- });
219
-
220
- if (!response.ok) {
221
- const errorData = await response.json().catch(() => null);
222
- throw new Error(errorData?.message || `HTTP ${response.status}: ${response.statusText}`);
223
- }
224
-
225
- const result = await response.json();
226
-
227
- if (result.success) {
228
- // Защитная проверка на существование result.data
229
- const data = result.data || {};
230
- return {
231
- deletedCount: data.deleted_count || 0,
232
- errors: data.errors || []
233
- };
234
- } else {
235
- throw new Error(result.message || 'Ошибка очистки изображений');
236
- }
237
-
238
- } catch (error) {
239
- console.error('Ошибка очистки неиспользуемых изображений:', error);
240
- throw error;
241
- }
242
- }
243
-
244
- /**
245
- * Получает информацию об изображении
246
- * @param {string} imageId - ID изображения
247
- */
248
- async getImageInfo(imageId) {
249
- try {
250
- const response = await fetch(`${this.deleteEndpoint}/${imageId}`, {
251
- method: 'GET',
252
- headers: {
253
- 'Accept': 'application/json',
254
- 'X-Requested-With': 'XMLHttpRequest'
255
- },
256
- credentials: 'same-origin'
257
- });
258
-
259
- if (!response.ok) {
260
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
261
- }
262
-
263
- const result = await response.json();
264
- return result.data;
265
-
266
- } catch (error) {
267
- console.error('Ошибка получения информации об изображении:', error);
268
- throw error;
269
- }
270
- }
271
-
272
139
  /**
273
140
  * Получает размеры изображения из файла или blob
274
141
  * @private
@@ -273,7 +273,7 @@ export class ToolEventRouter {
273
273
  return { dx: direction.x * step * ring, dy: direction.y * step * ring };
274
274
  };
275
275
 
276
- const emitAt = (src, name, imageId = null, offsetIndex = 0) => {
276
+ const emitAt = (src, name, offsetIndex = 0) => {
277
277
  if (!isCurrentDrop()) {
278
278
  logDropDebug(diagnostics, 'emit_skipped_stale_drop', { route: 'image', offsetIndex });
279
279
  return;
@@ -283,8 +283,7 @@ export class ToolEventRouter {
283
283
  x: x + offset,
284
284
  y: y + offset,
285
285
  src,
286
- name,
287
- imageId
286
+ name
288
287
  });
289
288
  };
290
289
 
@@ -329,13 +328,11 @@ export class ToolEventRouter {
329
328
  return null;
330
329
  }
331
330
  logDropDebug(diagnostics, 'image_upload_success', {
332
- fileName: uploadResult?.name || file.name || 'image',
333
- imageId: uploadResult?.imageId || uploadResult?.id || null
331
+ fileName: uploadResult?.name || file.name || 'image'
334
332
  });
335
333
  return {
336
334
  src: uploadResult.url,
337
335
  name: uploadResult.name,
338
- imageId: uploadResult.imageId || uploadResult.id || null,
339
336
  index
340
337
  };
341
338
  } else {
@@ -367,7 +364,7 @@ export class ToolEventRouter {
367
364
  });
368
365
  for (const placement of imagePlacements) {
369
366
  if (!placement) continue;
370
- emitAt(placement.src, placement.name, placement.imageId, placement.index);
367
+ emitAt(placement.src, placement.name, placement.index);
371
368
  }
372
369
  logDropDebug(diagnostics, 'drop_done', { route: 'image_files', itemsProcessed: imageFiles.length });
373
370
  return;
@@ -58,8 +58,7 @@ export class PlacementPayloadFactory {
58
58
  width,
59
59
  height,
60
60
  ...extraProperties
61
- },
62
- imageId: uploadResult.imageId || uploadResult.id
61
+ }
63
62
  });
64
63
  }
65
64