@xpert-ai/plugin-canvas 0.1.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 (109) hide show
  1. package/.xpertai-plugin/plugin.json +121 -0
  2. package/README.md +14 -0
  3. package/assets/composerIcon.svg +8 -0
  4. package/assets/logo.svg +47 -0
  5. package/dist/index.d.ts +15 -0
  6. package/dist/index.d.ts.map +1 -0
  7. package/dist/index.js +144 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/lib/canvas-agent-response.d.ts +40 -0
  10. package/dist/lib/canvas-agent-response.d.ts.map +1 -0
  11. package/dist/lib/canvas-agent-response.js +54 -0
  12. package/dist/lib/canvas-agent-response.js.map +1 -0
  13. package/dist/lib/canvas-snapshot.validation.d.ts +52 -0
  14. package/dist/lib/canvas-snapshot.validation.d.ts.map +1 -0
  15. package/dist/lib/canvas-snapshot.validation.js +223 -0
  16. package/dist/lib/canvas-snapshot.validation.js.map +1 -0
  17. package/dist/lib/canvas-view.provider.d.ts +14 -0
  18. package/dist/lib/canvas-view.provider.d.ts.map +1 -0
  19. package/dist/lib/canvas-view.provider.js +529 -0
  20. package/dist/lib/canvas-view.provider.js.map +1 -0
  21. package/dist/lib/canvas.middleware.d.ts +10 -0
  22. package/dist/lib/canvas.middleware.d.ts.map +1 -0
  23. package/dist/lib/canvas.middleware.js +277 -0
  24. package/dist/lib/canvas.middleware.js.map +1 -0
  25. package/dist/lib/canvas.plugin.d.ts +8 -0
  26. package/dist/lib/canvas.plugin.d.ts.map +1 -0
  27. package/dist/lib/canvas.plugin.js +27 -0
  28. package/dist/lib/canvas.plugin.js.map +1 -0
  29. package/dist/lib/canvas.service.d.ts +1514 -0
  30. package/dist/lib/canvas.service.d.ts.map +1 -0
  31. package/dist/lib/canvas.service.js +1290 -0
  32. package/dist/lib/canvas.service.js.map +1 -0
  33. package/dist/lib/canvas.templates.d.ts +3 -0
  34. package/dist/lib/canvas.templates.d.ts.map +1 -0
  35. package/dist/lib/canvas.templates.js +79 -0
  36. package/dist/lib/canvas.templates.js.map +1 -0
  37. package/dist/lib/constants.d.ts +26 -0
  38. package/dist/lib/constants.d.ts.map +1 -0
  39. package/dist/lib/constants.js +44 -0
  40. package/dist/lib/constants.js.map +1 -0
  41. package/dist/lib/entities/canvas-action-log.entity.d.ts +18 -0
  42. package/dist/lib/entities/canvas-action-log.entity.d.ts.map +1 -0
  43. package/dist/lib/entities/canvas-action-log.entity.js +69 -0
  44. package/dist/lib/entities/canvas-action-log.entity.js.map +1 -0
  45. package/dist/lib/entities/canvas-document-version.entity.d.ts +27 -0
  46. package/dist/lib/entities/canvas-document-version.entity.d.ts.map +1 -0
  47. package/dist/lib/entities/canvas-document-version.entity.js +106 -0
  48. package/dist/lib/entities/canvas-document-version.entity.js.map +1 -0
  49. package/dist/lib/entities/canvas-document.entity.d.ts +36 -0
  50. package/dist/lib/entities/canvas-document.entity.d.ts.map +1 -0
  51. package/dist/lib/entities/canvas-document.entity.js +142 -0
  52. package/dist/lib/entities/canvas-document.entity.js.map +1 -0
  53. package/dist/lib/entities/index.d.ts +4 -0
  54. package/dist/lib/entities/index.d.ts.map +1 -0
  55. package/dist/lib/entities/index.js +4 -0
  56. package/dist/lib/entities/index.js.map +1 -0
  57. package/dist/lib/remote-components/canvas-workbench/app.css +1 -0
  58. package/dist/lib/remote-components/canvas-workbench/app.js +1707 -0
  59. package/dist/lib/remote-components/canvas-workbench/src/autosave.d.ts +39 -0
  60. package/dist/lib/remote-components/canvas-workbench/src/autosave.d.ts.map +1 -0
  61. package/dist/lib/remote-components/canvas-workbench/src/autosave.js +155 -0
  62. package/dist/lib/remote-components/canvas-workbench/src/autosave.js.map +1 -0
  63. package/dist/lib/remote-components/canvas-workbench/src/i18n.d.ts +3 -0
  64. package/dist/lib/remote-components/canvas-workbench/src/i18n.d.ts.map +1 -0
  65. package/dist/lib/remote-components/canvas-workbench/src/i18n.js +79 -0
  66. package/dist/lib/remote-components/canvas-workbench/src/i18n.js.map +1 -0
  67. package/dist/lib/remote-components/canvas-workbench/src/react-dom-client-shim.d.ts +4 -0
  68. package/dist/lib/remote-components/canvas-workbench/src/react-dom-client-shim.d.ts.map +1 -0
  69. package/dist/lib/remote-components/canvas-workbench/src/react-dom-client-shim.js +7 -0
  70. package/dist/lib/remote-components/canvas-workbench/src/react-dom-client-shim.js.map +1 -0
  71. package/dist/lib/remote-components/canvas-workbench/src/react-dom-shim.d.ts +14 -0
  72. package/dist/lib/remote-components/canvas-workbench/src/react-dom-shim.d.ts.map +1 -0
  73. package/dist/lib/remote-components/canvas-workbench/src/react-dom-shim.js +14 -0
  74. package/dist/lib/remote-components/canvas-workbench/src/react-dom-shim.js.map +1 -0
  75. package/dist/lib/remote-components/canvas-workbench/src/react-jsx-runtime-shim.d.ts +7 -0
  76. package/dist/lib/remote-components/canvas-workbench/src/react-jsx-runtime-shim.d.ts.map +1 -0
  77. package/dist/lib/remote-components/canvas-workbench/src/react-jsx-runtime-shim.js +26 -0
  78. package/dist/lib/remote-components/canvas-workbench/src/react-jsx-runtime-shim.js.map +1 -0
  79. package/dist/lib/remote-components/canvas-workbench/src/react-shim.d.ts +47 -0
  80. package/dist/lib/remote-components/canvas-workbench/src/react-shim.d.ts.map +1 -0
  81. package/dist/lib/remote-components/canvas-workbench/src/react-shim.js +39 -0
  82. package/dist/lib/remote-components/canvas-workbench/src/react-shim.js.map +1 -0
  83. package/dist/lib/remote-components/canvas-workbench/src/runtime.d.ts +45 -0
  84. package/dist/lib/remote-components/canvas-workbench/src/runtime.d.ts.map +1 -0
  85. package/dist/lib/remote-components/canvas-workbench/src/runtime.js +199 -0
  86. package/dist/lib/remote-components/canvas-workbench/src/runtime.js.map +1 -0
  87. package/dist/lib/remote-components/canvas-workbench/src/selection-context.d.ts +55 -0
  88. package/dist/lib/remote-components/canvas-workbench/src/selection-context.d.ts.map +1 -0
  89. package/dist/lib/remote-components/canvas-workbench/src/selection-context.js +49 -0
  90. package/dist/lib/remote-components/canvas-workbench/src/selection-context.js.map +1 -0
  91. package/dist/lib/remote-components/canvas-workbench/src/styles.d.ts +2 -0
  92. package/dist/lib/remote-components/canvas-workbench/src/styles.d.ts.map +1 -0
  93. package/dist/lib/remote-components/canvas-workbench/src/styles.js +333 -0
  94. package/dist/lib/remote-components/canvas-workbench/src/styles.js.map +1 -0
  95. package/dist/lib/remote-components/canvas-workbench/src/tool-event-refresh.d.ts +14 -0
  96. package/dist/lib/remote-components/canvas-workbench/src/tool-event-refresh.d.ts.map +1 -0
  97. package/dist/lib/remote-components/canvas-workbench/src/tool-event-refresh.js +19 -0
  98. package/dist/lib/remote-components/canvas-workbench/src/tool-event-refresh.js.map +1 -0
  99. package/dist/lib/remote-components/canvas-workbench/src/vendor.d.ts +9 -0
  100. package/dist/lib/remote-components/canvas-workbench/src/vendor.d.ts.map +1 -0
  101. package/dist/lib/remote-components/canvas-workbench/src/vendor.js +7 -0
  102. package/dist/lib/remote-components/canvas-workbench/src/vendor.js.map +1 -0
  103. package/dist/lib/types.d.ts +180 -0
  104. package/dist/lib/types.d.ts.map +1 -0
  105. package/dist/lib/types.js +2 -0
  106. package/dist/lib/types.js.map +1 -0
  107. package/dist/xpert-canvas-assistant.yaml +168 -0
  108. package/package.json +91 -0
  109. package/skills/canvas-agent-skill/SKILL.md +144 -0
@@ -0,0 +1,1290 @@
1
+ import { __decorate, __metadata, __param } from "tslib";
2
+ import { BadRequestException, Inject, Injectable, NotFoundException, Optional } from '@nestjs/common';
3
+ import { InjectRepository } from '@nestjs/typeorm';
4
+ import { createHash, randomUUID } from 'node:crypto';
5
+ import { extname } from 'node:path';
6
+ import { generateKeyBetween } from 'fractional-indexing';
7
+ import { Repository } from 'typeorm';
8
+ import { XPERT_RUNTIME_CAPABILITIES_TOKEN } from '@xpert-ai/plugin-sdk';
9
+ import { CanvasActionLog, CanvasDocument, CanvasDocumentVersion } from './entities/index.js';
10
+ import { CanvasSnapshotValidationError, compactRecordForAgent, compactSnapshotForAgent, createEmptyCanvasSnapshot, isCanvasSnapshot, normalizeCanvasSnapshot, summarizeSnapshot } from './canvas-snapshot.validation.js';
11
+ import { CANVAS_WORKSPACE_FILES_RUNTIME_CAPABILITY } from './types.js';
12
+ const SNAPSHOT_IMAGE_MIME_TYPES = new Set(['image/png', 'image/jpeg', 'image/webp']);
13
+ let CanvasService = class CanvasService {
14
+ constructor(documentRepository, versionRepository, logRepository, runtimeCapabilities) {
15
+ this.documentRepository = documentRepository;
16
+ this.versionRepository = versionRepository;
17
+ this.logRepository = logRepository;
18
+ this.runtimeCapabilities = runtimeCapabilities;
19
+ }
20
+ async createDocument(scope, input) {
21
+ const title = normalizeRequired(input.title, 'Canvas title is required.');
22
+ const document = await this.documentRepository.save(this.documentRepository.create({
23
+ ...scopedCreate(scope),
24
+ assistantId: scope.assistantId ?? null,
25
+ conversationId: scope.conversationId ?? null,
26
+ title,
27
+ description: normalizeOptional(input.description),
28
+ kind: input.kind ?? 'canvas',
29
+ status: 'draft',
30
+ tags: normalizeStringArray(input.tags),
31
+ source: normalizeOptional(input.source),
32
+ currentVersionNumber: 0,
33
+ lastEditedById: scope.userId ?? null,
34
+ lastEditedAt: new Date()
35
+ }));
36
+ await this.writeLog(scope, {
37
+ documentId: document.id,
38
+ action: 'document_created',
39
+ actorType: scope.assistantId ? 'agent' : 'user',
40
+ message: `Canvas "${title}" was created.`,
41
+ snapshot: { title, kind: document.kind, source: document.source }
42
+ });
43
+ if (hasSnapshotContent(input)) {
44
+ await this.createVersion(scope, document, {
45
+ sourceType: 'agent_snapshot',
46
+ snapshot: normalizeSnapshotInput(input.snapshot),
47
+ viewState: normalizeObject(input.viewState),
48
+ selectionSummary: normalizeObject(input.selectionSummary),
49
+ snapshotImage: input.snapshotImage,
50
+ changeSummary: normalizeOptional(input.changeSummary) ?? 'Initial canvas'
51
+ });
52
+ }
53
+ return this.getDocument(scope, { documentId: document.id, includeSnapshot: true });
54
+ }
55
+ async saveSnapshot(scope, input) {
56
+ const document = await this.requireDocument(scope, input.documentId);
57
+ const version = await this.createVersion(scope, document, {
58
+ sourceType: input.sourceType ?? 'agent_snapshot',
59
+ snapshot: normalizeSnapshotInput(input.snapshot),
60
+ viewState: normalizeObject(input.viewState),
61
+ selectionSummary: normalizeObject(input.selectionSummary),
62
+ snapshotImage: input.snapshotImage,
63
+ changeSummary: normalizeOptional(input.changeSummary) ?? 'Canvas snapshot saved'
64
+ });
65
+ await this.writeLog(scope, {
66
+ documentId: document.id,
67
+ versionId: version.id,
68
+ action: 'snapshot_saved',
69
+ actorType: input.sourceType === 'workbench' ? 'user' : 'agent',
70
+ message: input.changeSummary,
71
+ snapshot: summarizeSnapshot(version.snapshot)
72
+ });
73
+ return {
74
+ success: true,
75
+ message: 'Canvas snapshot was saved.',
76
+ document: await this.getDocument(scope, { documentId: document.id, includeSnapshot: true }),
77
+ version
78
+ };
79
+ }
80
+ async autosaveSnapshot(scope, input) {
81
+ const document = await this.requireDocument(scope, input.documentId);
82
+ const snapshot = normalizeSnapshotInput(input.snapshot);
83
+ const viewState = normalizeObject(input.viewState);
84
+ const selectionSummary = normalizeObject(input.selectionSummary);
85
+ const imageFields = await this.uploadSnapshotImage(scope, document, input.snapshotImage, {
86
+ mode: 'current',
87
+ sourceType: 'workbench',
88
+ versionNumber: document.currentVersionNumber ?? 0
89
+ });
90
+ const autosaveUpdatedAt = new Date();
91
+ const savedDocument = await this.documentRepository.save({
92
+ ...document,
93
+ autosaveSnapshot: snapshot,
94
+ autosaveViewState: viewState,
95
+ autosaveSelectionSummary: selectionSummary,
96
+ autosaveUpdatedAt,
97
+ autosaveBaseVersionId: document.currentVersionId ?? null,
98
+ ...imageFields,
99
+ status: document.status === 'archived' ? document.status : 'draft',
100
+ lastEditedById: scope.userId ?? null,
101
+ lastEditedAt: autosaveUpdatedAt
102
+ });
103
+ return {
104
+ success: true,
105
+ message: 'Canvas working copy was autosaved.',
106
+ document: await this.getDocument(scope, { documentId: savedDocument.id, includeSnapshot: true }),
107
+ autosave: {
108
+ documentId: savedDocument.id,
109
+ autosaveUpdatedAt,
110
+ autosaveBaseVersionId: savedDocument.autosaveBaseVersionId,
111
+ snapshotImagePath: savedDocument.snapshotImagePath,
112
+ snapshotImageUrl: savedDocument.snapshotImageUrl,
113
+ snapshotImageChecksum: savedDocument.snapshotImageChecksum,
114
+ snapshotSummary: summarizeSnapshot(snapshot)
115
+ }
116
+ };
117
+ }
118
+ async patchRecords(scope, input) {
119
+ const document = await this.requireDocument(scope, input.documentId);
120
+ const currentState = await this.getCurrentCanvasState(scope, document);
121
+ const snapshot = structuredClone(currentState.snapshot);
122
+ if (!isCanvasSnapshot(snapshot)) {
123
+ throw new BadRequestException('Current canvas snapshot is invalid.');
124
+ }
125
+ const nextStore = { ...snapshot.store };
126
+ const removeIds = collectUniqueStrings(input.removeRecordIds ?? [], 'removeRecordIds');
127
+ for (const id of removeIds) {
128
+ delete nextStore[id];
129
+ }
130
+ for (const record of input.putRecords ?? []) {
131
+ if (!isPlainObject(record) || typeof record.id !== 'string' || !record.id.trim()) {
132
+ throw new BadRequestException('Every putRecords item must be a tldraw record with a non-empty id.');
133
+ }
134
+ nextStore[record.id] = record;
135
+ }
136
+ const normalized = normalizeSnapshotInput({
137
+ ...snapshot,
138
+ store: nextStore
139
+ });
140
+ const viewState = {
141
+ ...normalizeObject(currentState.viewState),
142
+ ...normalizeObject(input.viewStatePatch)
143
+ };
144
+ const version = await this.createVersion(scope, document, {
145
+ sourceType: 'agent_patch',
146
+ snapshot: normalized,
147
+ viewState,
148
+ selectionSummary: input.selectionSummary === undefined ? normalizeObject(currentState.selectionSummary) : normalizeObject(input.selectionSummary),
149
+ changeSummary: normalizeOptional(input.changeSummary) ?? 'Canvas records patched'
150
+ });
151
+ await this.writeLog(scope, {
152
+ documentId: document.id,
153
+ versionId: version.id,
154
+ action: 'records_patched',
155
+ actorType: 'agent',
156
+ message: input.changeSummary,
157
+ snapshot: {
158
+ putRecordCount: input.putRecords?.length ?? 0,
159
+ removeRecordIds: removeIds,
160
+ summary: summarizeSnapshot(normalized)
161
+ }
162
+ });
163
+ return {
164
+ success: true,
165
+ message: 'Canvas records were patched.',
166
+ document: await this.getDocument(scope, { documentId: document.id, includeSnapshot: false }),
167
+ version: compactVersion(version)
168
+ };
169
+ }
170
+ async insertImage(scope, input) {
171
+ const dataUrl = normalizeImageDataUrl(input);
172
+ const imageSize = readImageSizeFromDataUrl(dataUrl, input);
173
+ const document = input.documentId
174
+ ? await this.requireDocument(scope, input.documentId)
175
+ : (await this.createDocument(scope, {
176
+ title: input.title ?? 'Untitled Canvas',
177
+ description: input.description,
178
+ kind: input.kind ?? 'image-board',
179
+ source: 'agent_image'
180
+ })).item;
181
+ const currentState = await this.getCurrentCanvasState(scope, document);
182
+ const snapshot = structuredClone(currentState.snapshot);
183
+ if (!isCanvasSnapshot(snapshot)) {
184
+ throw new BadRequestException('Current canvas snapshot is invalid.');
185
+ }
186
+ const store = { ...snapshot.store };
187
+ const anchorShape = input.anchorShapeId ? store[input.anchorShapeId] : null;
188
+ const pageId = normalizeOptional(input.pageId) ?? findPageIdForShape(store, anchorShape?.id) ?? findFirstPageId(store);
189
+ if (!pageId || !store[pageId]) {
190
+ throw new BadRequestException('Could not determine target canvas page.');
191
+ }
192
+ const holderPlacement = isAiImageHolder(anchorShape);
193
+ const parentId = holderPlacement ? anchorShape.id : anchorShape?.parentId && store[anchorShape.parentId]?.typeName === 'page' ? anchorShape.parentId : pageId;
194
+ const anchorBounds = holderPlacement ? null : pageBoundsForShape(store, anchorShape);
195
+ const holderBounds = holderPlacement ? pageBoundsForShape(store, anchorShape) : null;
196
+ const matchAnchor = input.matchAnchor !== false && (holderBounds || anchorBounds);
197
+ const displayWidth = positiveNumber(input.displayWidth) ?? (matchAnchor ? (holderBounds ?? anchorBounds)?.w : Math.min(imageSize.width, 512)) ?? imageSize.width;
198
+ const displayHeight = positiveNumber(input.displayHeight) ??
199
+ (matchAnchor ? (holderBounds ?? anchorBounds)?.h : Math.round(displayWidth * (imageSize.height / imageSize.width))) ??
200
+ imageSize.height;
201
+ const bounds = holderPlacement
202
+ ? { x: 0, y: 0, w: displayWidth, h: displayHeight }
203
+ : choosePlacement({
204
+ store,
205
+ pageId,
206
+ parentId,
207
+ anchorShape,
208
+ width: displayWidth,
209
+ height: displayHeight,
210
+ margin: Math.max(0, input.margin ?? 40),
211
+ placement: input.placement ?? 'right'
212
+ });
213
+ const seed = sanitizeIdPart(input.fileName ?? `canvas-image-${Date.now()}`);
214
+ const assetId = uniqueRecordId(store, 'asset', seed);
215
+ const shapeId = uniqueRecordId(store, 'shape', seed);
216
+ const fileName = sanitizeFileName(input.fileName ?? `${seed}.${extensionFromMimeType(dataUrl.mimeType).replace('.', '')}`, dataUrl.mimeType);
217
+ const fileSize = Buffer.byteLength(dataUrl.base64, 'base64');
218
+ const index = chooseIndex(store, parentId);
219
+ const assetRecord = {
220
+ id: assetId,
221
+ typeName: 'asset',
222
+ type: 'image',
223
+ props: {
224
+ name: fileName,
225
+ src: dataUrl.url,
226
+ w: imageSize.width,
227
+ h: imageSize.height,
228
+ fileSize,
229
+ mimeType: dataUrl.mimeType,
230
+ isAnimated: false
231
+ },
232
+ meta: normalizeObject(input.assetMeta)
233
+ };
234
+ const shapeRecord = {
235
+ x: bounds.x,
236
+ y: bounds.y,
237
+ rotation: 0,
238
+ isLocked: false,
239
+ opacity: 1,
240
+ meta: {
241
+ ...(holderPlacement ? { canvasGeneratedForAiImageHolder: anchorShape.id } : { canvasGeneratedStandalone: true }),
242
+ ...normalizeObject(input.shapeMeta)
243
+ },
244
+ id: shapeId,
245
+ type: 'image',
246
+ props: {
247
+ w: bounds.w,
248
+ h: bounds.h,
249
+ assetId,
250
+ playing: true,
251
+ url: '',
252
+ crop: null,
253
+ flipX: false,
254
+ flipY: false,
255
+ altText: normalizeOptional(input.altText) ?? 'Canvas inserted image'
256
+ },
257
+ parentId,
258
+ index,
259
+ typeName: 'shape'
260
+ };
261
+ store[assetId] = assetRecord;
262
+ store[shapeId] = shapeRecord;
263
+ const normalized = normalizeSnapshotInput({
264
+ ...snapshot,
265
+ store
266
+ });
267
+ const version = await this.createVersion(scope, document, {
268
+ sourceType: 'agent_image',
269
+ snapshot: normalized,
270
+ viewState: normalizeObject(currentState.viewState),
271
+ selectionSummary: {
272
+ selectedShapes: [shapeRecord],
273
+ insertedShapeId: shapeId,
274
+ source: 'canvas_insert_image'
275
+ },
276
+ changeSummary: normalizeOptional(input.changeSummary) ?? 'Image inserted'
277
+ });
278
+ await this.writeLog(scope, {
279
+ documentId: document.id,
280
+ versionId: version.id,
281
+ action: 'image_inserted',
282
+ actorType: 'agent',
283
+ message: input.changeSummary,
284
+ snapshot: {
285
+ assetId,
286
+ shapeId,
287
+ pageId,
288
+ parentId,
289
+ anchorShapeId: input.anchorShapeId,
290
+ bounds,
291
+ imageSize
292
+ }
293
+ });
294
+ return {
295
+ success: true,
296
+ message: 'Image was inserted into the canvas.',
297
+ document: await this.getDocument(scope, { documentId: document.id, includeSnapshot: false }),
298
+ version: compactVersion(version),
299
+ insertion: {
300
+ pageId,
301
+ parentId,
302
+ anchorShapeId: input.anchorShapeId,
303
+ assetId,
304
+ shapeId,
305
+ index,
306
+ imageSize,
307
+ bounds
308
+ }
309
+ };
310
+ }
311
+ async searchDocuments(scope, query = {}) {
312
+ const page = Math.max(1, query.page ?? 1);
313
+ const pageSize = Math.max(1, Math.min(query.pageSize ?? 20, 100));
314
+ const search = query.search?.trim().toLowerCase() ?? '';
315
+ const documents = await this.documentRepository.find({
316
+ where: scopedWhere(scope),
317
+ order: {
318
+ updatedAt: 'DESC'
319
+ }
320
+ });
321
+ const filtered = documents.filter((document) => {
322
+ if (query.status && document.status !== query.status) {
323
+ return false;
324
+ }
325
+ if (query.kind && document.kind !== query.kind) {
326
+ return false;
327
+ }
328
+ if (!search) {
329
+ return true;
330
+ }
331
+ return [document.title, document.description, document.kind, ...(document.tags ?? [])]
332
+ .filter(isString)
333
+ .some((value) => value.toLowerCase().includes(search));
334
+ });
335
+ const start = (page - 1) * pageSize;
336
+ return {
337
+ items: filtered.slice(start, start + pageSize),
338
+ total: filtered.length,
339
+ page,
340
+ pageSize,
341
+ search
342
+ };
343
+ }
344
+ async getDocument(scope, input) {
345
+ const document = await this.requireDocument(scope, input.documentId);
346
+ const versionLimit = Math.max(1, Math.min(input.versionLimit ?? 20, 100));
347
+ const logLimit = Math.max(1, Math.min(input.logLimit ?? 10, 50));
348
+ const logWhere = scopedWhere(scope, { documentId: document.id });
349
+ const [versions, logs] = await Promise.all([
350
+ this.versionRepository.find({
351
+ where: scopedWhere(scope, { documentId: document.id }),
352
+ order: {
353
+ versionNumber: 'DESC'
354
+ },
355
+ take: versionLimit
356
+ }),
357
+ input.includeLogs
358
+ ? this.logRepository.find({
359
+ where: logWhere,
360
+ order: {
361
+ createdAt: 'DESC'
362
+ },
363
+ take: logLimit
364
+ })
365
+ : Promise.resolve([])
366
+ ]);
367
+ const currentVersion = versions.find((version) => version.id === document.currentVersionId) ?? versions[0] ?? null;
368
+ const requestedVersion = selectRequestedVersion({ currentVersion, versions }, input);
369
+ const explicitVersion = hasExplicitVersionRequest(input);
370
+ const workingCopy = formatWorkingCopy(document, Boolean(input.includeSnapshot));
371
+ const effectiveSnapshot = explicitVersion ? requestedVersion?.snapshot ?? null : document.autosaveSnapshot ?? currentVersion?.snapshot ?? null;
372
+ const effectiveSceneSource = explicitVersion ? 'version' : document.autosaveSnapshot ? 'autosave' : 'version';
373
+ return {
374
+ item: document,
375
+ currentVersion: formatVersionForResponse(currentVersion, Boolean(input.includeSnapshot)),
376
+ requestedVersion: requestedVersion && requestedVersion.id !== currentVersion?.id ? formatVersionForResponse(requestedVersion, Boolean(input.includeSnapshot)) : null,
377
+ workingCopy,
378
+ versions: versions.map((version) => compactVersion(version)),
379
+ logs,
380
+ snapshotSummary: summarizeSnapshot(effectiveSnapshot),
381
+ scene: input.includeSnapshot ? effectiveSnapshot : compactSnapshotForAgent(effectiveSnapshot),
382
+ sceneSource: effectiveSceneSource,
383
+ snapshotImagePath: explicitVersion ? requestedVersion?.snapshotImagePath ?? null : document.snapshotImagePath ?? currentVersion?.snapshotImagePath ?? null,
384
+ snapshotImageUrl: explicitVersion ? requestedVersion?.snapshotImageUrl ?? null : document.snapshotImageUrl ?? currentVersion?.snapshotImageUrl ?? null,
385
+ snapshotImageUpdatedAt: explicitVersion ? requestedVersion?.createdAt ?? null : document.autosaveUpdatedAt ?? currentVersion?.createdAt ?? null,
386
+ nextActions: [
387
+ 'Open the Canvas Workbench to review or edit the canvas.',
388
+ 'Use canvas_get_record for exact record JSON before targeted edits.',
389
+ 'Use canvas_patch_records for small changes or canvas_save_snapshot for a full replacement.'
390
+ ]
391
+ };
392
+ }
393
+ async getRecord(scope, input) {
394
+ const explicitVersion = hasExplicitVersionRequest(input);
395
+ const payload = await this.getDocument(scope, {
396
+ documentId: input.documentId,
397
+ versionId: input.versionId,
398
+ versionNumber: input.versionNumber,
399
+ includeSnapshot: true
400
+ });
401
+ const version = explicitVersion ? payload.requestedVersion ?? payload.currentVersion : payload.currentVersion;
402
+ const snapshot = explicitVersion && isVersionWithSnapshot(version) ? version.snapshot : payload.scene;
403
+ if (!isCanvasSnapshot(snapshot)) {
404
+ throw new NotFoundException('Canvas snapshot was not found.');
405
+ }
406
+ const record = snapshot.store[input.recordId];
407
+ if (!record) {
408
+ throw new NotFoundException('Canvas record was not found.');
409
+ }
410
+ return {
411
+ document: payload.item,
412
+ version: compactVersion(isVersionWithSnapshot(version) ? version : null),
413
+ sceneSource: payload.sceneSource,
414
+ record
415
+ };
416
+ }
417
+ async getRecordForAgent(scope, input) {
418
+ const result = await this.getRecord(scope, input);
419
+ return {
420
+ ...result,
421
+ record: compactRecordForAgent(result.record)
422
+ };
423
+ }
424
+ async updateDocumentStatus(scope, input) {
425
+ const document = await this.requireDocument(scope, input.documentId);
426
+ document.status = input.status;
427
+ document.lastEditedById = scope.userId ?? null;
428
+ document.lastEditedAt = new Date();
429
+ await this.documentRepository.save(document);
430
+ await this.writeLog(scope, {
431
+ documentId: document.id,
432
+ action: input.status === 'archived' ? 'document_archived' : 'status_updated',
433
+ actorType: scope.assistantId ? 'agent' : 'user',
434
+ message: input.reason,
435
+ snapshot: { status: input.status }
436
+ });
437
+ return {
438
+ success: true,
439
+ message: `Canvas status updated to ${input.status}.`,
440
+ document
441
+ };
442
+ }
443
+ async reportFailure(scope, input) {
444
+ await this.writeLog(scope, {
445
+ documentId: input.documentId,
446
+ versionId: input.versionId,
447
+ action: 'failure_reported',
448
+ actorType: 'agent',
449
+ message: input.operation,
450
+ errorMessage: normalizeRequired(input.errorMessage, 'Error message is required.'),
451
+ snapshot: {
452
+ operation: input.operation,
453
+ recoverable: input.recoverable ?? true,
454
+ evidence: input.evidence
455
+ }
456
+ });
457
+ return {
458
+ success: true,
459
+ message: 'Canvas failure was recorded.'
460
+ };
461
+ }
462
+ async restoreVersion(scope, documentId, versionId, changeSummary) {
463
+ const document = await this.requireDocument(scope, documentId);
464
+ const version = await this.versionRepository.findOne({
465
+ where: scopedWhere(scope, { documentId: document.id, id: versionId })
466
+ });
467
+ if (!version) {
468
+ throw new NotFoundException('Canvas version was not found.');
469
+ }
470
+ const restored = await this.createVersion(scope, document, {
471
+ sourceType: 'restore',
472
+ snapshot: normalizeSnapshotInput(version.snapshot),
473
+ viewState: normalizeObject(version.viewState),
474
+ selectionSummary: normalizeObject(version.selectionSummary),
475
+ changeSummary: normalizeOptional(changeSummary) ?? `Restored version ${version.versionNumber}`
476
+ });
477
+ await this.writeLog(scope, {
478
+ documentId: document.id,
479
+ versionId: restored.id,
480
+ action: 'version_restored',
481
+ actorType: scope.assistantId ? 'agent' : 'user',
482
+ message: changeSummary,
483
+ snapshot: { restoredVersionId: version.id, restoredVersionNumber: version.versionNumber }
484
+ });
485
+ return {
486
+ success: true,
487
+ message: 'Canvas version was restored.',
488
+ document: await this.getDocument(scope, { documentId: document.id, includeSnapshot: true }),
489
+ version: restored
490
+ };
491
+ }
492
+ async deleteDocument(scope, documentId) {
493
+ const document = await this.requireDocument(scope, documentId);
494
+ const versions = await this.versionRepository.find({
495
+ where: scopedWhere(scope, { documentId: document.id })
496
+ });
497
+ const workspaceFiles = this.runtimeCapabilities?.get(CANVAS_WORKSPACE_FILES_RUNTIME_CAPABILITY);
498
+ if (workspaceFiles) {
499
+ const cleanupTargets = [
500
+ document.snapshotImagePath
501
+ ? {
502
+ filePath: document.snapshotImagePath,
503
+ scope: resolveDocumentWorkspaceScope(scope, document)
504
+ }
505
+ : null,
506
+ ...versions.map((version) => version.snapshotImagePath
507
+ ? {
508
+ filePath: version.snapshotImagePath,
509
+ scope: resolveVersionWorkspaceScope(scope, version)
510
+ }
511
+ : null)
512
+ ].filter(Boolean);
513
+ await Promise.all(cleanupTargets.map(async (target) => {
514
+ try {
515
+ await workspaceFiles.deleteFile({
516
+ ...target.scope,
517
+ filePath: target.filePath
518
+ });
519
+ }
520
+ catch {
521
+ // Workspace file cleanup is best-effort; database deletion is authoritative.
522
+ }
523
+ }));
524
+ }
525
+ await Promise.all([
526
+ this.versionRepository.delete(scopedWhere(scope, { documentId: document.id })),
527
+ this.logRepository.delete(scopedWhere(scope, { documentId: document.id }))
528
+ ]);
529
+ await this.documentRepository.delete(scopedWhere(scope, { id: document.id }));
530
+ return {
531
+ success: true,
532
+ message: 'Canvas document was deleted.'
533
+ };
534
+ }
535
+ async prepareAssistantPrompt(scope, input) {
536
+ const document = await this.requireDocument(scope, input.documentId);
537
+ const instruction = normalizeOptional(input.instruction) ?? '请根据当前画布内容继续协助我。';
538
+ const imagePath = normalizeOptional(document.snapshotImagePath);
539
+ const lines = [
540
+ `当前 Canvas 文档 id: ${document.id}`,
541
+ `标题: ${document.title}`,
542
+ `当前版本: ${document.currentVersionNumber ?? 0}`,
543
+ `当前场景来源: ${document.autosaveSnapshot ? 'autosave' : 'version'}`,
544
+ ...(imagePath
545
+ ? [
546
+ `当前 viewport 快照图片路径: ${imagePath}`,
547
+ `快照更新时间: ${document.autosaveUpdatedAt?.toISOString?.() ?? ''}`,
548
+ `请先调用 view_image,参数 path 为 "${imagePath}",读取这张图片后再分析画布视觉内容。`
549
+ ]
550
+ : ['当前画布还没有可供 view_image 读取的 viewport 快照图片;如需视觉分析,请先在 Workbench 自动保存或保存画布。']),
551
+ '',
552
+ instruction,
553
+ '',
554
+ '如果需要精确修改 shape,请使用 Canvas middleware tools 读取 document/record 后再 patch。'
555
+ ];
556
+ return {
557
+ commandKey: 'assistant.chat.send_message',
558
+ payload: {
559
+ text: lines.join('\n')
560
+ },
561
+ documentId: document.id,
562
+ snapshotImagePath: imagePath ?? null,
563
+ snapshotImageUpdatedAt: document.autosaveUpdatedAt ?? null
564
+ };
565
+ }
566
+ workspaceFiles() {
567
+ const files = this.runtimeCapabilities?.get(CANVAS_WORKSPACE_FILES_RUNTIME_CAPABILITY);
568
+ if (!files) {
569
+ throw new BadRequestException('Xpert workspace file runtime capability is required for Canvas snapshot image storage.');
570
+ }
571
+ return files;
572
+ }
573
+ async uploadSnapshotImage(scope, document, image, options) {
574
+ if (!image) {
575
+ throw new BadRequestException('Canvas snapshot image is required.');
576
+ }
577
+ const normalized = normalizeSnapshotImageData(image);
578
+ const checksumValue = checksum(normalized.buffer);
579
+ const workspaceScope = resolveDocumentWorkspaceScope(scope, document);
580
+ const documentId = requireEntityId(document.id, 'Canvas document id is required.');
581
+ const folder = options.mode === 'current'
582
+ ? buildCanvasSnapshotFolder(documentId)
583
+ : `${buildCanvasSnapshotFolder(documentId)}/versions`;
584
+ const fileName = options.mode === 'current' ? 'current.png' : `v${options.versionNumber}-${checksumValue.slice(0, 8)}.png`;
585
+ const workspaceFile = await this.workspaceFiles().uploadBuffer({
586
+ ...workspaceScope,
587
+ buffer: normalized.buffer,
588
+ originalName: normalizeOptional(image.fileName) ?? fileName,
589
+ mimeType: normalized.mimeType,
590
+ size: normalized.buffer.byteLength,
591
+ folder,
592
+ fileName,
593
+ metadata: {
594
+ documentType: 'canvas-snapshot-image',
595
+ documentId,
596
+ versionNumber: options.versionNumber,
597
+ sourceType: options.sourceType,
598
+ snapshotImageRole: options.mode,
599
+ pageId: normalizeOptional(image.pageId),
600
+ capturedAt: normalizeOptional(image.capturedAt),
601
+ width: image.width,
602
+ height: image.height,
603
+ camera: image.camera
604
+ }
605
+ });
606
+ return {
607
+ snapshotImagePath: workspaceFile.filePath,
608
+ snapshotImageUrl: normalizeOptional(workspaceFile.fileUrl) ?? normalizeOptional(workspaceFile.url),
609
+ snapshotImageMimeType: normalized.mimeType,
610
+ snapshotImageSize: normalized.buffer.byteLength,
611
+ snapshotImageChecksum: checksumValue,
612
+ workspaceCatalog: workspaceScope.catalog,
613
+ workspaceScopeId: workspaceScope.scopeId
614
+ };
615
+ }
616
+ async getCurrentCanvasState(scope, document) {
617
+ const currentVersion = await this.getCurrentVersion(scope, document);
618
+ if (isCanvasSnapshot(document.autosaveSnapshot)) {
619
+ return {
620
+ source: 'autosave',
621
+ version: currentVersion,
622
+ snapshot: document.autosaveSnapshot,
623
+ viewState: normalizeObject(document.autosaveViewState),
624
+ selectionSummary: normalizeObject(document.autosaveSelectionSummary)
625
+ };
626
+ }
627
+ return {
628
+ source: 'version',
629
+ version: currentVersion,
630
+ snapshot: currentVersion?.snapshot ?? createEmptyCanvasSnapshot(),
631
+ viewState: normalizeObject(currentVersion?.viewState),
632
+ selectionSummary: normalizeObject(currentVersion?.selectionSummary)
633
+ };
634
+ }
635
+ async createVersion(scope, document, input) {
636
+ const versionNumber = (document.currentVersionNumber ?? 0) + 1;
637
+ const [latestImageFields, versionImageFields] = input.snapshotImage
638
+ ? await Promise.all([
639
+ this.uploadSnapshotImage(scope, document, input.snapshotImage, {
640
+ mode: 'current',
641
+ sourceType: input.sourceType ?? 'workbench',
642
+ versionNumber
643
+ }),
644
+ this.uploadSnapshotImage(scope, document, input.snapshotImage, {
645
+ mode: 'version',
646
+ sourceType: input.sourceType ?? 'workbench',
647
+ versionNumber
648
+ })
649
+ ])
650
+ : [null, null];
651
+ const version = await this.versionRepository.save(this.versionRepository.create({
652
+ ...scopedCreate(scope),
653
+ documentId: document.id,
654
+ versionNumber,
655
+ sourceType: input.sourceType,
656
+ snapshot: input.snapshot,
657
+ viewState: input.viewState ?? null,
658
+ selectionSummary: input.selectionSummary ?? null,
659
+ ...(versionImageFields ?? {}),
660
+ changeSummary: normalizeOptional(input.changeSummary),
661
+ assistantId: scope.assistantId ?? null,
662
+ conversationId: scope.conversationId ?? null
663
+ }));
664
+ document.currentVersionId = version.id;
665
+ document.currentVersionNumber = version.versionNumber;
666
+ document.autosaveSnapshot = input.snapshot;
667
+ document.autosaveViewState = input.viewState ?? null;
668
+ document.autosaveSelectionSummary = input.selectionSummary ?? null;
669
+ document.autosaveUpdatedAt = new Date();
670
+ document.autosaveBaseVersionId = version.id;
671
+ if (latestImageFields) {
672
+ document.snapshotImagePath = latestImageFields.snapshotImagePath;
673
+ document.snapshotImageUrl = latestImageFields.snapshotImageUrl;
674
+ document.snapshotImageMimeType = latestImageFields.snapshotImageMimeType;
675
+ document.snapshotImageSize = latestImageFields.snapshotImageSize;
676
+ document.snapshotImageChecksum = latestImageFields.snapshotImageChecksum;
677
+ document.workspaceCatalog = latestImageFields.workspaceCatalog;
678
+ document.workspaceScopeId = latestImageFields.workspaceScopeId;
679
+ }
680
+ document.lastEditedById = scope.userId ?? scope.assistantId ?? null;
681
+ document.lastEditedAt = new Date();
682
+ await this.documentRepository.save(document);
683
+ return version;
684
+ }
685
+ async getCurrentVersion(scope, document) {
686
+ if (!document.currentVersionId) {
687
+ return null;
688
+ }
689
+ return this.versionRepository.findOne({
690
+ where: scopedWhere(scope, { id: document.currentVersionId, documentId: document.id })
691
+ });
692
+ }
693
+ async requireDocument(scope, documentId) {
694
+ const document = await this.documentRepository.findOne({
695
+ where: scopedWhere(scope, { id: normalizeRequired(documentId, 'Canvas document id is required.') })
696
+ });
697
+ if (!document) {
698
+ throw new NotFoundException('Canvas document was not found.');
699
+ }
700
+ return document;
701
+ }
702
+ async writeLog(scope, input) {
703
+ return this.logRepository.save(this.logRepository.create({
704
+ ...scopedCreate(scope),
705
+ documentId: input.documentId ?? null,
706
+ versionId: input.versionId ?? null,
707
+ action: input.action,
708
+ actorType: input.actorType ?? 'system',
709
+ actorId: scope.userId ?? scope.assistantId ?? null,
710
+ message: normalizeOptional(input.message),
711
+ errorMessage: normalizeOptional(input.errorMessage),
712
+ snapshot: input.snapshot
713
+ }));
714
+ }
715
+ };
716
+ CanvasService = __decorate([
717
+ Injectable(),
718
+ __param(0, InjectRepository(CanvasDocument)),
719
+ __param(1, InjectRepository(CanvasDocumentVersion)),
720
+ __param(2, InjectRepository(CanvasActionLog)),
721
+ __param(3, Optional()),
722
+ __param(3, Inject(XPERT_RUNTIME_CAPABILITIES_TOKEN)),
723
+ __metadata("design:paramtypes", [Repository,
724
+ Repository,
725
+ Repository, Object])
726
+ ], CanvasService);
727
+ export { CanvasService };
728
+ function normalizeSnapshotInput(snapshot) {
729
+ try {
730
+ return normalizeCanvasSnapshot(snapshot ?? createEmptyCanvasSnapshot());
731
+ }
732
+ catch (error) {
733
+ if (error instanceof CanvasSnapshotValidationError) {
734
+ throw new BadRequestException(error.message);
735
+ }
736
+ throw error;
737
+ }
738
+ }
739
+ function normalizeSnapshotImageData(input) {
740
+ const dataUrl = normalizeOptional(input.dataUrl);
741
+ let mimeType;
742
+ let base64;
743
+ if (dataUrl) {
744
+ const parsed = parseDataUrl(dataUrl);
745
+ if (!parsed) {
746
+ throw new BadRequestException('snapshotImage.dataUrl must be a valid image data URL.');
747
+ }
748
+ mimeType = parsed.mimeType;
749
+ base64 = parsed.base64;
750
+ }
751
+ else {
752
+ base64 = normalizeRequired(input.base64, 'snapshotImage base64 or dataUrl is required.');
753
+ mimeType = normalizeMimeType(input.mimeType);
754
+ }
755
+ if (!SNAPSHOT_IMAGE_MIME_TYPES.has(mimeType)) {
756
+ throw new BadRequestException('Canvas snapshot images must be PNG, JPEG, or WebP.');
757
+ }
758
+ const buffer = Buffer.from(base64, 'base64');
759
+ if (buffer.byteLength === 0) {
760
+ throw new BadRequestException('Canvas snapshot image is empty.');
761
+ }
762
+ if (detectImageMimeType(buffer) !== mimeType) {
763
+ throw new BadRequestException('Canvas snapshot image bytes do not match the declared MIME type.');
764
+ }
765
+ return {
766
+ mimeType,
767
+ buffer
768
+ };
769
+ }
770
+ function normalizeImageDataUrl(input) {
771
+ const explicit = normalizeOptional(input.dataUrl);
772
+ if (explicit) {
773
+ const parsed = parseDataUrl(explicit);
774
+ if (!parsed) {
775
+ throw new BadRequestException('dataUrl must be a valid image data URL.');
776
+ }
777
+ return parsed;
778
+ }
779
+ const base64 = normalizeOptional(input.base64);
780
+ if (!base64) {
781
+ throw new BadRequestException('Canvas image insertion requires dataUrl or base64.');
782
+ }
783
+ const mimeType = normalizeMimeType(input.mimeType);
784
+ return {
785
+ mimeType,
786
+ base64,
787
+ url: `data:${mimeType};base64,${base64}`
788
+ };
789
+ }
790
+ function detectImageMimeType(buffer) {
791
+ if (buffer.length >= 24 && buffer[0] === 0x89 && buffer.toString('ascii', 1, 4) === 'PNG') {
792
+ return 'image/png';
793
+ }
794
+ if (buffer.length >= 3 && buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) {
795
+ return 'image/jpeg';
796
+ }
797
+ if (buffer.length >= 16 && buffer.toString('ascii', 0, 4) === 'RIFF' && buffer.toString('ascii', 8, 12) === 'WEBP') {
798
+ return 'image/webp';
799
+ }
800
+ return null;
801
+ }
802
+ function parseDataUrl(value) {
803
+ const match = /^data:([^;,]+)?(?:;[^,]*)?,(.*)$/s.exec(value);
804
+ if (!match) {
805
+ return null;
806
+ }
807
+ const mimeType = normalizeMimeType(match[1]);
808
+ const encoded = match[2];
809
+ const isBase64 = /^data:[^,]*;base64,/i.test(value);
810
+ const base64 = isBase64 ? encoded : Buffer.from(decodeURIComponent(encoded)).toString('base64');
811
+ return {
812
+ mimeType,
813
+ base64,
814
+ url: `data:${mimeType};base64,${base64}`
815
+ };
816
+ }
817
+ function readImageSizeFromDataUrl(dataUrl, input) {
818
+ const explicitWidth = positiveNumber(input.width);
819
+ const explicitHeight = positiveNumber(input.height);
820
+ if (explicitWidth && explicitHeight) {
821
+ return { width: explicitWidth, height: explicitHeight };
822
+ }
823
+ const buffer = Buffer.from(dataUrl.base64, 'base64');
824
+ const detected = detectImageSize(buffer);
825
+ return {
826
+ width: explicitWidth ?? detected?.width ?? positiveNumber(input.displayWidth) ?? 512,
827
+ height: explicitHeight ?? detected?.height ?? positiveNumber(input.displayHeight) ?? 512
828
+ };
829
+ }
830
+ function detectImageSize(buffer) {
831
+ if (buffer.length >= 24 && buffer.toString('ascii', 1, 4) === 'PNG') {
832
+ return { width: buffer.readUInt32BE(16), height: buffer.readUInt32BE(20) };
833
+ }
834
+ if (buffer.length >= 10 && buffer[0] === 0xff && buffer[1] === 0xd8) {
835
+ let offset = 2;
836
+ while (offset < buffer.length - 9) {
837
+ if (buffer[offset] !== 0xff) {
838
+ break;
839
+ }
840
+ const marker = buffer[offset + 1];
841
+ const size = buffer.readUInt16BE(offset + 2);
842
+ if ((marker >= 0xc0 && marker <= 0xc3) || (marker >= 0xc5 && marker <= 0xc7) || (marker >= 0xc9 && marker <= 0xcb) || (marker >= 0xcd && marker <= 0xcf)) {
843
+ return { width: buffer.readUInt16BE(offset + 7), height: buffer.readUInt16BE(offset + 5) };
844
+ }
845
+ offset += 2 + size;
846
+ }
847
+ }
848
+ if (buffer.length >= 30 && buffer.toString('ascii', 0, 4) === 'RIFF' && buffer.toString('ascii', 8, 12) === 'WEBP') {
849
+ const chunk = buffer.toString('ascii', 12, 16);
850
+ if (chunk === 'VP8X') {
851
+ return {
852
+ width: 1 + buffer.readUIntLE(24, 3),
853
+ height: 1 + buffer.readUIntLE(27, 3)
854
+ };
855
+ }
856
+ }
857
+ return null;
858
+ }
859
+ function selectRequestedVersion(payload, input) {
860
+ const versions = Array.isArray(payload.versions) ? payload.versions : [];
861
+ if (input.versionId) {
862
+ const version = versions.find((candidate) => candidate.id === input.versionId);
863
+ if (!version) {
864
+ throw new NotFoundException('Requested Canvas version was not found.');
865
+ }
866
+ return version;
867
+ }
868
+ if (input.versionNumber !== undefined) {
869
+ const version = versions.find((candidate) => candidate.versionNumber === input.versionNumber);
870
+ if (!version) {
871
+ throw new NotFoundException('Requested Canvas version was not found.');
872
+ }
873
+ return version;
874
+ }
875
+ return payload.currentVersion ?? versions[0] ?? null;
876
+ }
877
+ function hasExplicitVersionRequest(input) {
878
+ return Boolean(input.versionId || input.versionNumber !== undefined);
879
+ }
880
+ function formatWorkingCopy(document, includeSnapshot) {
881
+ if (!isCanvasSnapshot(document.autosaveSnapshot)) {
882
+ return null;
883
+ }
884
+ return {
885
+ snapshot: includeSnapshot ? document.autosaveSnapshot : undefined,
886
+ scene: includeSnapshot ? undefined : compactSnapshotForAgent(document.autosaveSnapshot),
887
+ viewState: document.autosaveViewState,
888
+ selectionSummary: document.autosaveSelectionSummary,
889
+ autosaveUpdatedAt: document.autosaveUpdatedAt,
890
+ autosaveBaseVersionId: document.autosaveBaseVersionId,
891
+ snapshotImagePath: document.snapshotImagePath,
892
+ snapshotImageUrl: document.snapshotImageUrl,
893
+ snapshotImageMimeType: document.snapshotImageMimeType,
894
+ snapshotImageSize: document.snapshotImageSize,
895
+ snapshotImageChecksum: document.snapshotImageChecksum,
896
+ snapshotSummary: summarizeSnapshot(document.autosaveSnapshot)
897
+ };
898
+ }
899
+ function scopedCreate(scope) {
900
+ return {
901
+ tenantId: scope.tenantId,
902
+ organizationId: scope.organizationId ?? null,
903
+ workspaceId: scope.workspaceId ?? null,
904
+ projectId: scope.projectId ?? null,
905
+ createdById: scope.userId ?? null
906
+ };
907
+ }
908
+ function scopedWhere(scope, extra) {
909
+ const where = {
910
+ tenantId: scope.tenantId
911
+ };
912
+ if (scope.organizationId != null) {
913
+ where.organizationId = scope.organizationId;
914
+ }
915
+ if (scope.projectId != null) {
916
+ where.projectId = scope.projectId;
917
+ }
918
+ else if (scope.workspaceId != null) {
919
+ where.workspaceId = scope.workspaceId;
920
+ }
921
+ return {
922
+ ...where,
923
+ ...(extra ?? {})
924
+ };
925
+ }
926
+ function formatVersionForResponse(version, includeSnapshot) {
927
+ if (!version) {
928
+ return null;
929
+ }
930
+ return includeSnapshot
931
+ ? version
932
+ : {
933
+ ...compactVersion(version),
934
+ scene: compactSnapshotForAgent(version.snapshot)
935
+ };
936
+ }
937
+ function compactVersion(version) {
938
+ if (!version) {
939
+ return null;
940
+ }
941
+ return {
942
+ id: version.id,
943
+ documentId: version.documentId,
944
+ versionNumber: version.versionNumber,
945
+ sourceType: version.sourceType,
946
+ viewState: version.viewState,
947
+ selectionSummary: version.selectionSummary,
948
+ snapshotImagePath: version.snapshotImagePath,
949
+ snapshotImageUrl: version.snapshotImageUrl,
950
+ snapshotImageMimeType: version.snapshotImageMimeType,
951
+ snapshotImageSize: version.snapshotImageSize,
952
+ snapshotImageChecksum: version.snapshotImageChecksum,
953
+ changeSummary: version.changeSummary,
954
+ createdAt: version.createdAt,
955
+ snapshotSummary: summarizeSnapshot(version.snapshot)
956
+ };
957
+ }
958
+ function isVersionWithSnapshot(value) {
959
+ return Boolean(value && 'snapshot' in value);
960
+ }
961
+ function hasSnapshotContent(input) {
962
+ return Boolean(input.snapshot || input.viewState || input.selectionSummary);
963
+ }
964
+ function normalizeRequired(value, message) {
965
+ const normalized = normalizeOptional(value);
966
+ if (!normalized) {
967
+ throw new BadRequestException(message);
968
+ }
969
+ return normalized;
970
+ }
971
+ function normalizeOptional(value) {
972
+ const normalized = value?.trim();
973
+ return normalized ? normalized : undefined;
974
+ }
975
+ function normalizeStringArray(values) {
976
+ const normalized = (values ?? []).map((value) => normalizeOptional(value)).filter(isString);
977
+ return normalized.length ? Array.from(new Set(normalized)) : undefined;
978
+ }
979
+ function normalizeObject(value) {
980
+ return isPlainObject(value) ? value : {};
981
+ }
982
+ function normalizeMimeType(value) {
983
+ const normalized = typeof value === 'string' && value.trim() ? value.trim().toLowerCase() : 'image/png';
984
+ return normalized.startsWith('image/') ? normalized : 'image/png';
985
+ }
986
+ function collectUniqueStrings(values, label) {
987
+ const seen = new Set();
988
+ for (const value of values) {
989
+ const normalized = normalizeRequired(value, `${label} must contain non-empty strings.`);
990
+ if (seen.has(normalized)) {
991
+ throw new BadRequestException(`${label} contains duplicate id "${normalized}".`);
992
+ }
993
+ seen.add(normalized);
994
+ }
995
+ return Array.from(seen);
996
+ }
997
+ function isString(value) {
998
+ return typeof value === 'string' && value.trim().length > 0;
999
+ }
1000
+ function isPlainObject(value) {
1001
+ return Boolean(value && typeof value === 'object' && !Array.isArray(value));
1002
+ }
1003
+ function positiveNumber(value) {
1004
+ return typeof value === 'number' && Number.isFinite(value) && value > 0 ? Math.round(value) : null;
1005
+ }
1006
+ function sanitizeFileName(name, mimeType) {
1007
+ const extension = extname(name) || extensionFromMimeType(mimeType);
1008
+ const rawBase = name.slice(0, name.length - extname(name).length);
1009
+ const baseName = rawBase.replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, '');
1010
+ return `${baseName || 'image'}${extension}`;
1011
+ }
1012
+ function extensionFromMimeType(mimeType) {
1013
+ switch (mimeType) {
1014
+ case 'image/apng':
1015
+ return '.apng';
1016
+ case 'image/avif':
1017
+ return '.avif';
1018
+ case 'image/gif':
1019
+ return '.gif';
1020
+ case 'image/jpeg':
1021
+ return '.jpg';
1022
+ case 'image/svg+xml':
1023
+ return '.svg';
1024
+ case 'image/webp':
1025
+ return '.webp';
1026
+ default:
1027
+ return '.png';
1028
+ }
1029
+ }
1030
+ function sanitizeIdPart(value) {
1031
+ return (value
1032
+ .replace(/\.[^.]+$/, '')
1033
+ .replace(/[^a-zA-Z0-9_-]+/g, '-')
1034
+ .replace(/^-+|-+$/g, '')
1035
+ .slice(0, 80) || randomUUID());
1036
+ }
1037
+ function resolveDocumentWorkspaceScope(scope, document) {
1038
+ if (document.workspaceCatalog === 'projects' && document.workspaceScopeId) {
1039
+ return {
1040
+ tenantId: scope.tenantId,
1041
+ userId: scope.userId,
1042
+ catalog: 'projects',
1043
+ scopeId: document.workspaceScopeId,
1044
+ projectId: document.workspaceScopeId
1045
+ };
1046
+ }
1047
+ if (document.workspaceCatalog === 'xperts' && document.workspaceScopeId) {
1048
+ return {
1049
+ tenantId: scope.tenantId,
1050
+ userId: scope.userId,
1051
+ catalog: 'xperts',
1052
+ scopeId: document.workspaceScopeId,
1053
+ xpertId: document.workspaceScopeId,
1054
+ isolateByUser: false
1055
+ };
1056
+ }
1057
+ const projectId = normalizeOptional(scope.projectId) ?? normalizeOptional(document.projectId);
1058
+ if (projectId) {
1059
+ return {
1060
+ tenantId: scope.tenantId,
1061
+ userId: scope.userId,
1062
+ catalog: 'projects',
1063
+ scopeId: projectId,
1064
+ projectId
1065
+ };
1066
+ }
1067
+ const xpertId = normalizeOptional(scope.assistantId) ?? normalizeOptional(document.assistantId);
1068
+ if (!xpertId) {
1069
+ throw new BadRequestException('Canvas workspace storage requires an assistant or project scope.');
1070
+ }
1071
+ return {
1072
+ tenantId: scope.tenantId,
1073
+ userId: scope.userId,
1074
+ catalog: 'xperts',
1075
+ scopeId: xpertId,
1076
+ xpertId,
1077
+ isolateByUser: false
1078
+ };
1079
+ }
1080
+ function resolveVersionWorkspaceScope(scope, version) {
1081
+ if (version.workspaceCatalog === 'projects' && version.workspaceScopeId) {
1082
+ return {
1083
+ tenantId: scope.tenantId,
1084
+ userId: scope.userId,
1085
+ catalog: 'projects',
1086
+ scopeId: version.workspaceScopeId,
1087
+ projectId: version.workspaceScopeId
1088
+ };
1089
+ }
1090
+ if (version.workspaceCatalog === 'xperts' && version.workspaceScopeId) {
1091
+ return {
1092
+ tenantId: scope.tenantId,
1093
+ userId: scope.userId,
1094
+ catalog: 'xperts',
1095
+ scopeId: version.workspaceScopeId,
1096
+ xpertId: version.workspaceScopeId,
1097
+ isolateByUser: false
1098
+ };
1099
+ }
1100
+ const projectId = normalizeOptional(scope.projectId);
1101
+ if (projectId) {
1102
+ return {
1103
+ tenantId: scope.tenantId,
1104
+ userId: scope.userId,
1105
+ catalog: 'projects',
1106
+ scopeId: projectId,
1107
+ projectId
1108
+ };
1109
+ }
1110
+ const xpertId = normalizeOptional(scope.assistantId);
1111
+ if (!xpertId) {
1112
+ throw new BadRequestException('Canvas version workspace scope is missing.');
1113
+ }
1114
+ return {
1115
+ tenantId: scope.tenantId,
1116
+ userId: scope.userId,
1117
+ catalog: 'xperts',
1118
+ scopeId: xpertId,
1119
+ xpertId,
1120
+ isolateByUser: false
1121
+ };
1122
+ }
1123
+ function buildCanvasSnapshotFolder(documentId) {
1124
+ return `files/canvas/documents/${normalizePathSegment(documentId, 'Canvas document id is required.')}/snapshots`;
1125
+ }
1126
+ function normalizePathSegment(value, message) {
1127
+ const normalized = normalizeRequired(value, message);
1128
+ const segment = normalized.replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, '');
1129
+ if (!segment || segment === '.' || segment === '..') {
1130
+ throw new BadRequestException('Invalid workspace path segment.');
1131
+ }
1132
+ return segment;
1133
+ }
1134
+ function requireEntityId(value, message) {
1135
+ return normalizeRequired(value, message);
1136
+ }
1137
+ function checksum(buffer) {
1138
+ return createHash('sha256').update(buffer).digest('hex');
1139
+ }
1140
+ function uniqueRecordId(store, prefix, seed) {
1141
+ const cleanSeed = sanitizeIdPart(seed);
1142
+ let candidate = `${prefix}:${cleanSeed}`;
1143
+ let counter = 2;
1144
+ while (store[candidate]) {
1145
+ candidate = `${prefix}:${cleanSeed}-${counter}`;
1146
+ counter += 1;
1147
+ }
1148
+ return candidate;
1149
+ }
1150
+ function chooseIndex(store, parentId) {
1151
+ const siblingIndexes = Object.values(store)
1152
+ .filter((record) => record.typeName === 'shape' && record.parentId === parentId && typeof record.index === 'string')
1153
+ .map((record) => record.index)
1154
+ .sort();
1155
+ return generateKeyBetween(siblingIndexes.at(-1) ?? null, null);
1156
+ }
1157
+ function findFirstPageId(store) {
1158
+ return Object.values(store).find((record) => record.typeName === 'page')?.id;
1159
+ }
1160
+ function findPageIdForShape(store, shapeId) {
1161
+ if (!shapeId) {
1162
+ return null;
1163
+ }
1164
+ let record = store[shapeId];
1165
+ const visited = new Set();
1166
+ while (record && !visited.has(record.id)) {
1167
+ visited.add(record.id);
1168
+ if (record.typeName === 'page') {
1169
+ return record.id;
1170
+ }
1171
+ const parentId = record.parentId;
1172
+ if (!parentId) {
1173
+ break;
1174
+ }
1175
+ const parent = store[parentId];
1176
+ if (parent?.typeName === 'page') {
1177
+ return parent.id;
1178
+ }
1179
+ record = parent;
1180
+ }
1181
+ return null;
1182
+ }
1183
+ function isAiImageHolder(shape) {
1184
+ return Boolean(shape?.typeName === 'shape' && shape.type === 'frame' && (shape.meta?.canvasAiImageHolder === true || shape.meta?.cowartAiImageHolder === true));
1185
+ }
1186
+ function localBoundsForShape(shape) {
1187
+ if (!shape || shape.typeName !== 'shape') {
1188
+ return null;
1189
+ }
1190
+ if (shape.type === 'arrow') {
1191
+ const start = normalizePoint(shape.props?.start);
1192
+ const end = normalizePoint(shape.props?.end);
1193
+ const minX = Math.min(start.x, end.x);
1194
+ const minY = Math.min(start.y, end.y);
1195
+ const maxX = Math.max(start.x, end.x);
1196
+ const maxY = Math.max(start.y, end.y);
1197
+ return { x: minX, y: minY, w: Math.max(1, maxX - minX), h: Math.max(1, maxY - minY) };
1198
+ }
1199
+ const w = typeof shape.props?.w === 'number' ? shape.props.w : shape.type === 'text' ? 160 : 1;
1200
+ const h = typeof shape.props?.h === 'number' ? shape.props.h : shape.type === 'text' ? 40 : 1;
1201
+ return { x: 0, y: 0, w, h };
1202
+ }
1203
+ function pageBoundsForShape(store, shape) {
1204
+ const local = localBoundsForShape(shape);
1205
+ if (!shape || !local) {
1206
+ return null;
1207
+ }
1208
+ let x = finiteNumber(shape.x, 0) + local.x;
1209
+ let y = finiteNumber(shape.y, 0) + local.y;
1210
+ let parent = store[shape.parentId];
1211
+ const visited = new Set([shape.id]);
1212
+ while (parent?.typeName === 'shape' && !visited.has(parent.id)) {
1213
+ visited.add(parent.id);
1214
+ x += finiteNumber(parent.x, 0);
1215
+ y += finiteNumber(parent.y, 0);
1216
+ parent = store[parent.parentId];
1217
+ }
1218
+ return { x, y, w: local.w, h: local.h };
1219
+ }
1220
+ function choosePlacement(input) {
1221
+ if (input.placement === 'center' || !input.anchorShape) {
1222
+ return { x: 0, y: 0, w: input.width, h: input.height };
1223
+ }
1224
+ const anchorBounds = pageBoundsForShape(input.store, input.anchorShape);
1225
+ let x = anchorBounds ? anchorBounds.x + anchorBounds.w + input.margin : 0;
1226
+ let y = anchorBounds ? anchorBounds.y : 0;
1227
+ if (input.placement === 'left' && anchorBounds) {
1228
+ x = anchorBounds.x - input.width - input.margin;
1229
+ }
1230
+ if (input.placement === 'below' && anchorBounds) {
1231
+ x = anchorBounds.x;
1232
+ y = anchorBounds.y + anchorBounds.h + input.margin;
1233
+ }
1234
+ const pageShapes = getPageShapes(input.store, input.pageId);
1235
+ const obstacles = pageShapes
1236
+ .filter((shape) => shape.parentId === input.parentId && shape.id !== input.anchorShape?.id)
1237
+ .map((shape) => pageBoundsForShape(input.store, shape))
1238
+ .filter(Boolean);
1239
+ const stepX = Math.max(input.width + input.margin, 1);
1240
+ const stepY = Math.max(input.height + input.margin, 1);
1241
+ for (let attempt = 0; attempt < 60; attempt += 1) {
1242
+ const candidate = { x, y, w: input.width, h: input.height };
1243
+ if (!obstacles.some((bounds) => rectsOverlap(candidate, bounds, input.margin / 2))) {
1244
+ return candidate;
1245
+ }
1246
+ if (input.placement === 'below') {
1247
+ y += stepY;
1248
+ }
1249
+ else if (input.placement === 'left') {
1250
+ x -= stepX;
1251
+ }
1252
+ else {
1253
+ x += stepX;
1254
+ }
1255
+ }
1256
+ return { x, y, w: input.width, h: input.height };
1257
+ }
1258
+ function getPageShapes(store, pageId) {
1259
+ const shapes = [];
1260
+ const byParent = new Map();
1261
+ for (const record of Object.values(store)) {
1262
+ if (record.typeName !== 'shape') {
1263
+ continue;
1264
+ }
1265
+ const siblings = byParent.get(record.parentId) ?? [];
1266
+ siblings.push(record);
1267
+ byParent.set(record.parentId, siblings);
1268
+ }
1269
+ const queue = [...(byParent.get(pageId) ?? [])];
1270
+ while (queue.length > 0) {
1271
+ const shape = queue.shift();
1272
+ shapes.push(shape);
1273
+ queue.push(...(byParent.get(shape.id) ?? []));
1274
+ }
1275
+ return shapes;
1276
+ }
1277
+ function rectsOverlap(a, b, padding = 0) {
1278
+ return !(a.x + a.w + padding <= b.x || b.x + b.w + padding <= a.x || a.y + a.h + padding <= b.y || b.y + b.h + padding <= a.y);
1279
+ }
1280
+ function normalizePoint(value) {
1281
+ const object = isPlainObject(value) ? value : {};
1282
+ return {
1283
+ x: finiteNumber(object.x, 0),
1284
+ y: finiteNumber(object.y, 0)
1285
+ };
1286
+ }
1287
+ function finiteNumber(value, fallback) {
1288
+ return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
1289
+ }
1290
+ //# sourceMappingURL=canvas.service.js.map