@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.
- package/.xpertai-plugin/plugin.json +121 -0
- package/README.md +14 -0
- package/assets/composerIcon.svg +8 -0
- package/assets/logo.svg +47 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +144 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/canvas-agent-response.d.ts +40 -0
- package/dist/lib/canvas-agent-response.d.ts.map +1 -0
- package/dist/lib/canvas-agent-response.js +54 -0
- package/dist/lib/canvas-agent-response.js.map +1 -0
- package/dist/lib/canvas-snapshot.validation.d.ts +52 -0
- package/dist/lib/canvas-snapshot.validation.d.ts.map +1 -0
- package/dist/lib/canvas-snapshot.validation.js +223 -0
- package/dist/lib/canvas-snapshot.validation.js.map +1 -0
- package/dist/lib/canvas-view.provider.d.ts +14 -0
- package/dist/lib/canvas-view.provider.d.ts.map +1 -0
- package/dist/lib/canvas-view.provider.js +529 -0
- package/dist/lib/canvas-view.provider.js.map +1 -0
- package/dist/lib/canvas.middleware.d.ts +10 -0
- package/dist/lib/canvas.middleware.d.ts.map +1 -0
- package/dist/lib/canvas.middleware.js +277 -0
- package/dist/lib/canvas.middleware.js.map +1 -0
- package/dist/lib/canvas.plugin.d.ts +8 -0
- package/dist/lib/canvas.plugin.d.ts.map +1 -0
- package/dist/lib/canvas.plugin.js +27 -0
- package/dist/lib/canvas.plugin.js.map +1 -0
- package/dist/lib/canvas.service.d.ts +1514 -0
- package/dist/lib/canvas.service.d.ts.map +1 -0
- package/dist/lib/canvas.service.js +1290 -0
- package/dist/lib/canvas.service.js.map +1 -0
- package/dist/lib/canvas.templates.d.ts +3 -0
- package/dist/lib/canvas.templates.d.ts.map +1 -0
- package/dist/lib/canvas.templates.js +79 -0
- package/dist/lib/canvas.templates.js.map +1 -0
- package/dist/lib/constants.d.ts +26 -0
- package/dist/lib/constants.d.ts.map +1 -0
- package/dist/lib/constants.js +44 -0
- package/dist/lib/constants.js.map +1 -0
- package/dist/lib/entities/canvas-action-log.entity.d.ts +18 -0
- package/dist/lib/entities/canvas-action-log.entity.d.ts.map +1 -0
- package/dist/lib/entities/canvas-action-log.entity.js +69 -0
- package/dist/lib/entities/canvas-action-log.entity.js.map +1 -0
- package/dist/lib/entities/canvas-document-version.entity.d.ts +27 -0
- package/dist/lib/entities/canvas-document-version.entity.d.ts.map +1 -0
- package/dist/lib/entities/canvas-document-version.entity.js +106 -0
- package/dist/lib/entities/canvas-document-version.entity.js.map +1 -0
- package/dist/lib/entities/canvas-document.entity.d.ts +36 -0
- package/dist/lib/entities/canvas-document.entity.d.ts.map +1 -0
- package/dist/lib/entities/canvas-document.entity.js +142 -0
- package/dist/lib/entities/canvas-document.entity.js.map +1 -0
- package/dist/lib/entities/index.d.ts +4 -0
- package/dist/lib/entities/index.d.ts.map +1 -0
- package/dist/lib/entities/index.js +4 -0
- package/dist/lib/entities/index.js.map +1 -0
- package/dist/lib/remote-components/canvas-workbench/app.css +1 -0
- package/dist/lib/remote-components/canvas-workbench/app.js +1707 -0
- package/dist/lib/remote-components/canvas-workbench/src/autosave.d.ts +39 -0
- package/dist/lib/remote-components/canvas-workbench/src/autosave.d.ts.map +1 -0
- package/dist/lib/remote-components/canvas-workbench/src/autosave.js +155 -0
- package/dist/lib/remote-components/canvas-workbench/src/autosave.js.map +1 -0
- package/dist/lib/remote-components/canvas-workbench/src/i18n.d.ts +3 -0
- package/dist/lib/remote-components/canvas-workbench/src/i18n.d.ts.map +1 -0
- package/dist/lib/remote-components/canvas-workbench/src/i18n.js +79 -0
- package/dist/lib/remote-components/canvas-workbench/src/i18n.js.map +1 -0
- package/dist/lib/remote-components/canvas-workbench/src/react-dom-client-shim.d.ts +4 -0
- package/dist/lib/remote-components/canvas-workbench/src/react-dom-client-shim.d.ts.map +1 -0
- package/dist/lib/remote-components/canvas-workbench/src/react-dom-client-shim.js +7 -0
- package/dist/lib/remote-components/canvas-workbench/src/react-dom-client-shim.js.map +1 -0
- package/dist/lib/remote-components/canvas-workbench/src/react-dom-shim.d.ts +14 -0
- package/dist/lib/remote-components/canvas-workbench/src/react-dom-shim.d.ts.map +1 -0
- package/dist/lib/remote-components/canvas-workbench/src/react-dom-shim.js +14 -0
- package/dist/lib/remote-components/canvas-workbench/src/react-dom-shim.js.map +1 -0
- package/dist/lib/remote-components/canvas-workbench/src/react-jsx-runtime-shim.d.ts +7 -0
- package/dist/lib/remote-components/canvas-workbench/src/react-jsx-runtime-shim.d.ts.map +1 -0
- package/dist/lib/remote-components/canvas-workbench/src/react-jsx-runtime-shim.js +26 -0
- package/dist/lib/remote-components/canvas-workbench/src/react-jsx-runtime-shim.js.map +1 -0
- package/dist/lib/remote-components/canvas-workbench/src/react-shim.d.ts +47 -0
- package/dist/lib/remote-components/canvas-workbench/src/react-shim.d.ts.map +1 -0
- package/dist/lib/remote-components/canvas-workbench/src/react-shim.js +39 -0
- package/dist/lib/remote-components/canvas-workbench/src/react-shim.js.map +1 -0
- package/dist/lib/remote-components/canvas-workbench/src/runtime.d.ts +45 -0
- package/dist/lib/remote-components/canvas-workbench/src/runtime.d.ts.map +1 -0
- package/dist/lib/remote-components/canvas-workbench/src/runtime.js +199 -0
- package/dist/lib/remote-components/canvas-workbench/src/runtime.js.map +1 -0
- package/dist/lib/remote-components/canvas-workbench/src/selection-context.d.ts +55 -0
- package/dist/lib/remote-components/canvas-workbench/src/selection-context.d.ts.map +1 -0
- package/dist/lib/remote-components/canvas-workbench/src/selection-context.js +49 -0
- package/dist/lib/remote-components/canvas-workbench/src/selection-context.js.map +1 -0
- package/dist/lib/remote-components/canvas-workbench/src/styles.d.ts +2 -0
- package/dist/lib/remote-components/canvas-workbench/src/styles.d.ts.map +1 -0
- package/dist/lib/remote-components/canvas-workbench/src/styles.js +333 -0
- package/dist/lib/remote-components/canvas-workbench/src/styles.js.map +1 -0
- package/dist/lib/remote-components/canvas-workbench/src/tool-event-refresh.d.ts +14 -0
- package/dist/lib/remote-components/canvas-workbench/src/tool-event-refresh.d.ts.map +1 -0
- package/dist/lib/remote-components/canvas-workbench/src/tool-event-refresh.js +19 -0
- package/dist/lib/remote-components/canvas-workbench/src/tool-event-refresh.js.map +1 -0
- package/dist/lib/remote-components/canvas-workbench/src/vendor.d.ts +9 -0
- package/dist/lib/remote-components/canvas-workbench/src/vendor.d.ts.map +1 -0
- package/dist/lib/remote-components/canvas-workbench/src/vendor.js +7 -0
- package/dist/lib/remote-components/canvas-workbench/src/vendor.js.map +1 -0
- package/dist/lib/types.d.ts +180 -0
- package/dist/lib/types.d.ts.map +1 -0
- package/dist/lib/types.js +2 -0
- package/dist/lib/types.js.map +1 -0
- package/dist/xpert-canvas-assistant.yaml +168 -0
- package/package.json +91 -0
- 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
|