rol-websocket-channel 1.8.6 → 1.8.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +5 -4
- package/dist/message-handler.js +7 -0
- package/dist/src/admin/lib/fs.js +2 -1
- package/dist/src/admin/methods/artifacts.js +486 -50
- package/dist/src/admin/methods/experts.js +251 -0
- package/dist/src/admin/methods/index.js +3 -0
- package/dist/src/admin/methods/pairing.js +10 -1
- package/dist/src/admin/methods/sessions-extended.js +3 -3
- package/dist/src/admin/methods/system.js +116 -13
- package/dist/src/admin/tools/artifacts-tools.js +8 -1
- package/index.ts +5 -4
- package/message-handler.ts +8 -0
- package/package.json +1 -1
- package/src/admin/lib/fs.ts +2 -1
- package/src/admin/methods/artifacts.ts +715 -55
- package/src/admin/methods/experts.ts +367 -0
- package/src/admin/methods/index.ts +4 -0
- package/src/admin/methods/pairing.ts +12 -1
- package/src/admin/methods/sessions-extended.ts +3 -3
- package/src/admin/methods/system.ts +138 -13
- package/src/admin/tools/artifacts-tools.ts +8 -1
|
@@ -4,11 +4,14 @@ import path from 'node:path';
|
|
|
4
4
|
import { ensureDir, pathExists, readJsonFile } from '../lib/fs.js';
|
|
5
5
|
import { ensureInside } from '../lib/paths.js';
|
|
6
6
|
import { JsonRpcException, JSON_RPC_ERRORS } from '../jsonrpc.js';
|
|
7
|
+
import { findSessionRecord } from './sessions.js';
|
|
7
8
|
const INLINE_CONTENT_LIMIT_BYTES = 10 * 1024 * 1024;
|
|
8
9
|
const MIN_ARTIFACT_SIZE_BYTES = 1;
|
|
9
10
|
const ARTIFACT_MANIFEST_FILE = 'artifacts.json';
|
|
10
11
|
const MARKDOWN_SCAN_STATE_FILE = 'md-scan-state.json';
|
|
12
|
+
const ARTIFACT_ARCHIVE_STATE_FILE = 'artifact-archive-state.json';
|
|
11
13
|
const PLUGIN_WORKSPACE_STATE_DIR = path.join('.openclaw', 'rol-websocket-channel');
|
|
14
|
+
const SESSION_ARTIFACTS_ROOT_RELATIVE = 'artifacts/sessions';
|
|
12
15
|
const IGNORE_EXTENSIONS = new Set(['.tmp', '.part', '.crdownload']);
|
|
13
16
|
const IGNORE_FILE_NAMES = new Set(['.ds_store', 'thumbs.db', ARTIFACT_MANIFEST_FILE]);
|
|
14
17
|
const IGNORE_MARKDOWN_FILE_NAMES = new Set([
|
|
@@ -83,27 +86,46 @@ const MIME_BY_EXTENSION = {
|
|
|
83
86
|
export const listArtifacts = async (params, context) => {
|
|
84
87
|
const objectParams = expectOptionalObject(params);
|
|
85
88
|
const refresh = objectParams.refresh !== false;
|
|
89
|
+
const normalizedFilters = normalizeArtifactFilters(objectParams);
|
|
90
|
+
const filters = {
|
|
91
|
+
...normalizedFilters,
|
|
92
|
+
sessionId: await resolveCanonicalArtifactSessionId(context, normalizedFilters.sessionId)
|
|
93
|
+
};
|
|
94
|
+
const groupBy = normalizeArtifactGroupBy(objectParams.groupBy);
|
|
86
95
|
const manifest = refresh
|
|
87
|
-
? await refreshArtifactManifest(context)
|
|
96
|
+
? await refreshArtifactManifest(context, { sessionId: filters.sessionId })
|
|
88
97
|
: await readOrRefreshArtifactManifest(context);
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
98
|
+
const items = manifest.items.filter((item) => matchesArtifactFilters(item, filters));
|
|
99
|
+
const response = {
|
|
100
|
+
scope: filters.sessionId ? 'session' : 'workspace',
|
|
101
|
+
sessionId: filters.sessionId ?? null,
|
|
102
|
+
query: filters.query ?? null,
|
|
103
|
+
count: items.length,
|
|
92
104
|
manifestPath: manifest.manifestPath,
|
|
93
105
|
workspaceRoot: manifest.workspacePaths.workspaceRoot,
|
|
94
|
-
items:
|
|
106
|
+
items: items.map(artifactToJsonValue)
|
|
95
107
|
};
|
|
108
|
+
if (groupBy) {
|
|
109
|
+
response.groupBy = groupBy;
|
|
110
|
+
response.groups = buildArtifactGroups(items, groupBy);
|
|
111
|
+
}
|
|
112
|
+
return response;
|
|
96
113
|
};
|
|
97
114
|
export const refreshArtifacts = async (params, context) => {
|
|
98
115
|
const objectParams = expectOptionalObject(params);
|
|
99
|
-
const
|
|
116
|
+
const sessionId = await resolveCanonicalArtifactSessionId(context, optionalTrimmedString(objectParams.sessionId));
|
|
117
|
+
const manifest = await refreshArtifactManifest(context, { sessionId });
|
|
118
|
+
const items = sessionId
|
|
119
|
+
? manifest.items.filter((item) => item.sessionId === sessionId)
|
|
120
|
+
: manifest.items;
|
|
100
121
|
return {
|
|
101
122
|
ok: true,
|
|
102
|
-
scope: 'workspace',
|
|
103
|
-
|
|
123
|
+
scope: sessionId ? 'session' : 'workspace',
|
|
124
|
+
sessionId: sessionId ?? null,
|
|
125
|
+
count: items.length,
|
|
104
126
|
manifestPath: manifest.manifestPath,
|
|
105
127
|
workspaceRoot: manifest.workspacePaths.workspaceRoot,
|
|
106
|
-
items:
|
|
128
|
+
items: items.map(artifactToJsonValue)
|
|
107
129
|
};
|
|
108
130
|
};
|
|
109
131
|
export const getArtifactContent = async (params, context) => {
|
|
@@ -129,10 +151,12 @@ export const getArtifactContent = async (params, context) => {
|
|
|
129
151
|
export const ensureArtifactUploaded = async (params, context) => {
|
|
130
152
|
const objectParams = expectObject(params);
|
|
131
153
|
const artifact = await resolveArtifact(objectParams, context);
|
|
154
|
+
const scope = artifact.sessionId ? 'session' : 'workspace';
|
|
132
155
|
if (artifact.storageStatus === 'uploaded' && artifact.fileUrl) {
|
|
133
156
|
return {
|
|
134
157
|
ok: true,
|
|
135
|
-
scope
|
|
158
|
+
scope,
|
|
159
|
+
sessionId: artifact.sessionId ?? null,
|
|
136
160
|
uploaded: false,
|
|
137
161
|
artifactId: artifact.id,
|
|
138
162
|
objectKey: artifact.objectKey ?? null,
|
|
@@ -161,7 +185,8 @@ export const ensureArtifactUploaded = async (params, context) => {
|
|
|
161
185
|
});
|
|
162
186
|
return {
|
|
163
187
|
ok: true,
|
|
164
|
-
scope
|
|
188
|
+
scope,
|
|
189
|
+
sessionId: artifact.sessionId ?? null,
|
|
165
190
|
uploaded: true,
|
|
166
191
|
artifactId: artifact.id,
|
|
167
192
|
objectKey,
|
|
@@ -178,6 +203,9 @@ export const publishArtifact = async (params, context) => {
|
|
|
178
203
|
const uploadParams = {
|
|
179
204
|
artifactId: artifact.id
|
|
180
205
|
};
|
|
206
|
+
if (artifact.sessionId) {
|
|
207
|
+
uploadParams.sessionId = artifact.sessionId;
|
|
208
|
+
}
|
|
181
209
|
if (objectParams.presignedPostBody !== undefined) {
|
|
182
210
|
uploadParams.presignedPostBody = objectParams.presignedPostBody;
|
|
183
211
|
}
|
|
@@ -192,21 +220,32 @@ export const publishArtifact = async (params, context) => {
|
|
|
192
220
|
export const findLatestArtifacts = async (params, context) => {
|
|
193
221
|
const objectParams = expectOptionalObject(params);
|
|
194
222
|
const refresh = objectParams.refresh !== false;
|
|
223
|
+
const normalizedFilters = normalizeArtifactFindFilters(objectParams);
|
|
224
|
+
const filters = {
|
|
225
|
+
...normalizedFilters,
|
|
226
|
+
sessionId: await resolveCanonicalArtifactSessionId(context, normalizedFilters.sessionId)
|
|
227
|
+
};
|
|
195
228
|
const manifest = refresh
|
|
196
|
-
? await refreshArtifactManifest(context)
|
|
229
|
+
? await refreshArtifactManifest(context, { sessionId: filters.sessionId })
|
|
197
230
|
: await readOrRefreshArtifactManifest(context);
|
|
198
|
-
const
|
|
199
|
-
const
|
|
231
|
+
const groupBy = normalizeArtifactGroupBy(objectParams.groupBy);
|
|
232
|
+
const matchedItems = manifest.items
|
|
200
233
|
.filter((item) => matchesArtifactFindFilters(item, filters))
|
|
201
234
|
.sort(compareArtifactsByRecency)
|
|
202
|
-
.slice(0, filters.limit)
|
|
203
|
-
|
|
204
|
-
return {
|
|
235
|
+
.slice(0, filters.limit);
|
|
236
|
+
const response = {
|
|
205
237
|
ok: true,
|
|
206
|
-
scope: 'workspace',
|
|
207
|
-
|
|
208
|
-
|
|
238
|
+
scope: filters.sessionId ? 'session' : 'workspace',
|
|
239
|
+
sessionId: filters.sessionId ?? null,
|
|
240
|
+
query: filters.query ?? null,
|
|
241
|
+
count: matchedItems.length,
|
|
242
|
+
items: matchedItems.map(artifactToJsonValue)
|
|
209
243
|
};
|
|
244
|
+
if (groupBy) {
|
|
245
|
+
response.groupBy = groupBy;
|
|
246
|
+
response.groups = buildArtifactGroups(matchedItems, groupBy);
|
|
247
|
+
}
|
|
248
|
+
return response;
|
|
210
249
|
};
|
|
211
250
|
export const getArtifactPresignedPost = async (params, context) => {
|
|
212
251
|
const objectParams = expectObject(params);
|
|
@@ -248,11 +287,15 @@ export const markArtifactUploaded = async (params, context) => {
|
|
|
248
287
|
throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, 'At least one of objectKey or fileUrl is required');
|
|
249
288
|
}
|
|
250
289
|
const manifest = await readOrRefreshArtifactManifest(context);
|
|
251
|
-
const
|
|
290
|
+
const sessionId = await resolveCanonicalArtifactSessionId(context, optionalTrimmedString(objectParams.sessionId));
|
|
291
|
+
const artifact = findArtifact(manifest.items, objectParams, {
|
|
292
|
+
sessionId
|
|
293
|
+
});
|
|
252
294
|
if (!artifact) {
|
|
253
295
|
throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, 'Artifact not found', {
|
|
254
296
|
artifactId: objectParams.artifactId,
|
|
255
|
-
relativePath: objectParams.relativePath
|
|
297
|
+
relativePath: objectParams.relativePath,
|
|
298
|
+
sessionId
|
|
256
299
|
});
|
|
257
300
|
}
|
|
258
301
|
const updated = await persistUploadedArtifact(manifest.manifestPath, manifest.items, artifact.id, {
|
|
@@ -262,7 +305,8 @@ export const markArtifactUploaded = async (params, context) => {
|
|
|
262
305
|
});
|
|
263
306
|
return {
|
|
264
307
|
ok: true,
|
|
265
|
-
scope: 'workspace',
|
|
308
|
+
scope: updated.sessionId ? 'session' : 'workspace',
|
|
309
|
+
sessionId: updated.sessionId ?? null,
|
|
266
310
|
artifactId: artifact.id,
|
|
267
311
|
item: artifactToJsonValue(updated)
|
|
268
312
|
};
|
|
@@ -285,7 +329,8 @@ export function shouldIgnoreArtifactFile(fileName) {
|
|
|
285
329
|
const ext = path.extname(normalized);
|
|
286
330
|
return IGNORE_EXTENSIONS.has(ext);
|
|
287
331
|
}
|
|
288
|
-
async function refreshArtifactManifest(context) {
|
|
332
|
+
async function refreshArtifactManifest(context, options) {
|
|
333
|
+
const taggedSessionId = optionalTrimmedString(options?.sessionId);
|
|
289
334
|
const workspacePaths = await ensureWorkspacePaths(context.openclawRoot);
|
|
290
335
|
const { workspaceRoot, manifestPath } = workspacePaths;
|
|
291
336
|
const existing = await readExistingManifest(manifestPath);
|
|
@@ -293,28 +338,69 @@ async function refreshArtifactManifest(context) {
|
|
|
293
338
|
const loadedMarkdownScanState = await readMarkdownScanState(workspacePaths.markdownScanStatePath);
|
|
294
339
|
const hadMarkdownScanState = loadedMarkdownScanState !== null;
|
|
295
340
|
const markdownScanState = loadedMarkdownScanState ?? createMarkdownScanState();
|
|
296
|
-
const
|
|
341
|
+
const archiveState = await readArtifactArchiveState(workspacePaths.artifactArchiveStatePath);
|
|
342
|
+
const files = (await collectArtifactFiles(workspaceRoot))
|
|
343
|
+
.sort((left, right) => compareDiscoveredArtifactPaths(workspaceRoot, left, right));
|
|
344
|
+
const discoveredRelativePaths = new Set(files.map((filePath) => path.relative(workspaceRoot, filePath).replace(/\\/g, '/')));
|
|
345
|
+
const emittedRelativePaths = new Set();
|
|
297
346
|
const items = [];
|
|
298
|
-
for (const
|
|
299
|
-
|
|
347
|
+
for (const discoveredFullPath of files) {
|
|
348
|
+
let fullPath = discoveredFullPath;
|
|
349
|
+
let stat = await fs.stat(fullPath);
|
|
300
350
|
if (!stat.isFile() || stat.size < MIN_ARTIFACT_SIZE_BYTES) {
|
|
301
351
|
continue;
|
|
302
352
|
}
|
|
303
|
-
|
|
304
|
-
|
|
353
|
+
let relativePath = path.relative(workspaceRoot, fullPath).replace(/\\/g, '/');
|
|
354
|
+
let existingItem = existingByPath.get(relativePath);
|
|
305
355
|
const fileName = path.basename(fullPath);
|
|
356
|
+
const pathSessionId = extractSessionIdFromArtifactRelativePath(relativePath);
|
|
306
357
|
const isMarkdown = isMarkdownArtifactFile(fileName);
|
|
307
358
|
if (isMarkdown) {
|
|
308
|
-
if (!shouldIncludeMarkdownArtifactFile(relativePath, fileName, stat, markdownScanState, hadMarkdownScanState)) {
|
|
359
|
+
if (!shouldIncludeMarkdownArtifactFile(relativePath, fileName, stat, markdownScanState, hadMarkdownScanState, Boolean(taggedSessionId || pathSessionId))) {
|
|
309
360
|
continue;
|
|
310
361
|
}
|
|
311
362
|
}
|
|
312
363
|
else if (!shouldIncludeArtifactFile(fileName)) {
|
|
313
364
|
continue;
|
|
314
365
|
}
|
|
366
|
+
const archiveEntry = archiveState.files[relativePath];
|
|
367
|
+
if (!pathSessionId) {
|
|
368
|
+
if (archiveEntry && await artifactArchiveEntryIsCurrent(workspaceRoot, archiveEntry, stat)) {
|
|
369
|
+
if (!taggedSessionId) {
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
const archivedSessionId = await resolveArchiveStateSessionId(context, archiveEntry.sessionId);
|
|
373
|
+
if (archivedSessionId && (archivedSessionId !== taggedSessionId || archiveEntry.sessionId === taggedSessionId)) {
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
if (taggedSessionId) {
|
|
378
|
+
const archivedRelativePath = await copyArtifactFileToSessionDirectory(workspaceRoot, taggedSessionId, relativePath, fullPath, stat, existingByPath, archiveEntry);
|
|
379
|
+
archiveState.files[relativePath] = {
|
|
380
|
+
sessionId: taggedSessionId,
|
|
381
|
+
targetRelativePath: archivedRelativePath,
|
|
382
|
+
sizeBytes: stat.size,
|
|
383
|
+
mtimeMs: stat.mtimeMs,
|
|
384
|
+
archivedAt: new Date().toISOString()
|
|
385
|
+
};
|
|
386
|
+
if (discoveredRelativePaths.has(archivedRelativePath)) {
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
fullPath = ensureInside(workspaceRoot, path.join(workspaceRoot, archivedRelativePath));
|
|
390
|
+
relativePath = archivedRelativePath;
|
|
391
|
+
existingItem = existingByPath.get(relativePath);
|
|
392
|
+
stat = await fs.stat(fullPath);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
if (emittedRelativePaths.has(relativePath)) {
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
315
398
|
const ext = normalizeExtension(fileName);
|
|
316
399
|
const createdAt = normalizeTimestamp(stat.birthtime, stat.mtime);
|
|
317
|
-
|
|
400
|
+
let updatedAt = stat.mtime.toISOString();
|
|
401
|
+
let currentStat = stat;
|
|
402
|
+
const resolvedSessionId = resolveArtifactSessionId(relativePath, existingItem, taggedSessionId);
|
|
403
|
+
emittedRelativePaths.add(relativePath);
|
|
318
404
|
items.push({
|
|
319
405
|
id: existingItem?.id ?? buildArtifactId(relativePath),
|
|
320
406
|
fileName,
|
|
@@ -323,9 +409,10 @@ async function refreshArtifactManifest(context) {
|
|
|
323
409
|
category: classifyArtifactCategory(fileName),
|
|
324
410
|
mimeType: MIME_BY_EXTENSION[ext ?? ''] ?? 'application/octet-stream',
|
|
325
411
|
ext,
|
|
326
|
-
sizeBytes:
|
|
412
|
+
sizeBytes: currentStat.size,
|
|
327
413
|
storageStatus: existingItem?.storageStatus ?? 'local_only',
|
|
328
414
|
source: existingItem?.source ?? 'generated',
|
|
415
|
+
sessionId: resolvedSessionId,
|
|
329
416
|
previewable: isPreviewableCategory(classifyArtifactCategory(fileName)),
|
|
330
417
|
createdAt: existingItem?.createdAt ?? createdAt,
|
|
331
418
|
updatedAt,
|
|
@@ -338,6 +425,7 @@ async function refreshArtifactManifest(context) {
|
|
|
338
425
|
items.sort((a, b) => a.fileName.localeCompare(b.fileName));
|
|
339
426
|
markdownScanState.lastScanAt = new Date().toISOString();
|
|
340
427
|
await writeMarkdownScanState(workspacePaths.markdownScanStatePath, markdownScanState);
|
|
428
|
+
await writeArtifactArchiveState(workspacePaths.artifactArchiveStatePath, archiveState);
|
|
341
429
|
await writeArtifactManifest(manifestPath, items);
|
|
342
430
|
return { manifestPath, items, workspacePaths };
|
|
343
431
|
}
|
|
@@ -395,6 +483,30 @@ async function writeMarkdownScanState(statePath, state) {
|
|
|
395
483
|
await ensureDir(path.dirname(statePath));
|
|
396
484
|
await fs.writeFile(statePath, JSON.stringify(state, null, 2), 'utf8');
|
|
397
485
|
}
|
|
486
|
+
async function readArtifactArchiveState(statePath) {
|
|
487
|
+
if (!(await pathExists(statePath))) {
|
|
488
|
+
return createArtifactArchiveState();
|
|
489
|
+
}
|
|
490
|
+
const parsed = await readJsonFile(statePath);
|
|
491
|
+
if (!isObject(parsed) || !isObject(parsed.files)) {
|
|
492
|
+
return createArtifactArchiveState();
|
|
493
|
+
}
|
|
494
|
+
const files = {};
|
|
495
|
+
for (const [relativePath, value] of Object.entries(parsed.files)) {
|
|
496
|
+
const normalizedRelativePath = normalizeRelativePath(relativePath);
|
|
497
|
+
if (normalizedRelativePath && isArtifactArchiveStateFile(value)) {
|
|
498
|
+
files[normalizedRelativePath] = value;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
return {
|
|
502
|
+
version: 1,
|
|
503
|
+
files
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
async function writeArtifactArchiveState(statePath, state) {
|
|
507
|
+
await ensureDir(path.dirname(statePath));
|
|
508
|
+
await fs.writeFile(statePath, JSON.stringify(state, null, 2), 'utf8');
|
|
509
|
+
}
|
|
398
510
|
async function persistUploadedArtifact(manifestPath, items, artifactId, updates) {
|
|
399
511
|
const now = new Date().toISOString();
|
|
400
512
|
let updatedArtifact = null;
|
|
@@ -421,19 +533,20 @@ async function persistUploadedArtifact(manifestPath, items, artifactId, updates)
|
|
|
421
533
|
}
|
|
422
534
|
return updatedArtifact;
|
|
423
535
|
}
|
|
424
|
-
async function collectArtifactFiles(rootDir) {
|
|
536
|
+
async function collectArtifactFiles(rootDir, baseDir = rootDir) {
|
|
425
537
|
const files = [];
|
|
426
538
|
const entries = await fs.readdir(rootDir, { withFileTypes: true });
|
|
427
539
|
for (const entry of entries) {
|
|
428
|
-
|
|
540
|
+
const fullPath = path.join(rootDir, entry.name);
|
|
541
|
+
const relativePath = path.relative(baseDir, fullPath).replace(/\\/g, '/');
|
|
542
|
+
if (entry.isDirectory() && shouldIgnoreArtifactDirectory(entry.name, relativePath)) {
|
|
429
543
|
continue;
|
|
430
544
|
}
|
|
431
545
|
if (entry.isFile() && shouldIgnoreArtifactFile(entry.name)) {
|
|
432
546
|
continue;
|
|
433
547
|
}
|
|
434
|
-
const fullPath = path.join(rootDir, entry.name);
|
|
435
548
|
if (entry.isDirectory()) {
|
|
436
|
-
files.push(...await collectArtifactFiles(fullPath));
|
|
549
|
+
files.push(...await collectArtifactFiles(fullPath, baseDir));
|
|
437
550
|
continue;
|
|
438
551
|
}
|
|
439
552
|
if (entry.isFile()) {
|
|
@@ -442,15 +555,47 @@ async function collectArtifactFiles(rootDir) {
|
|
|
442
555
|
}
|
|
443
556
|
return files;
|
|
444
557
|
}
|
|
558
|
+
function compareDiscoveredArtifactPaths(workspaceRoot, left, right) {
|
|
559
|
+
const leftRelative = path.relative(workspaceRoot, left).replace(/\\/g, '/');
|
|
560
|
+
const rightRelative = path.relative(workspaceRoot, right).replace(/\\/g, '/');
|
|
561
|
+
const leftIsSessionArtifact = Boolean(extractSessionIdFromArtifactRelativePath(leftRelative));
|
|
562
|
+
const rightIsSessionArtifact = Boolean(extractSessionIdFromArtifactRelativePath(rightRelative));
|
|
563
|
+
if (leftIsSessionArtifact !== rightIsSessionArtifact) {
|
|
564
|
+
return leftIsSessionArtifact ? 1 : -1;
|
|
565
|
+
}
|
|
566
|
+
return leftRelative.localeCompare(rightRelative);
|
|
567
|
+
}
|
|
568
|
+
async function artifactArchiveEntryIsCurrent(workspaceRoot, entry, stat) {
|
|
569
|
+
if (entry.sizeBytes !== stat.size || entry.mtimeMs !== stat.mtimeMs) {
|
|
570
|
+
return false;
|
|
571
|
+
}
|
|
572
|
+
const targetFullPath = ensureInside(workspaceRoot, path.join(workspaceRoot, entry.targetRelativePath));
|
|
573
|
+
return await pathExists(targetFullPath);
|
|
574
|
+
}
|
|
575
|
+
async function copyArtifactFileToSessionDirectory(workspaceRoot, sessionId, sourceRelativePath, sourceFullPath, sourceStat, existingByPath, archiveEntry) {
|
|
576
|
+
const targetRelativePath = archiveEntry?.sessionId === sessionId
|
|
577
|
+
? archiveEntry.targetRelativePath
|
|
578
|
+
: await resolveAvailableSessionArtifactRelativePath(workspaceRoot, sessionId, sourceRelativePath, existingByPath);
|
|
579
|
+
const targetFullPath = ensureInside(workspaceRoot, path.join(workspaceRoot, targetRelativePath));
|
|
580
|
+
await ensureDir(path.dirname(targetFullPath));
|
|
581
|
+
await fs.copyFile(sourceFullPath, targetFullPath);
|
|
582
|
+
await fs.utimes(targetFullPath, new Date(), new Date(sourceStat.mtimeMs));
|
|
583
|
+
return targetRelativePath;
|
|
584
|
+
}
|
|
445
585
|
async function resolveArtifact(params, context, options) {
|
|
586
|
+
const sessionId = await resolveCanonicalArtifactSessionId(context, optionalTrimmedString(params.sessionId));
|
|
587
|
+
const preparedSelector = options?.refreshManifest && sessionId
|
|
588
|
+
? await prepareSessionArtifactSelector(params, context, sessionId)
|
|
589
|
+
: params;
|
|
446
590
|
const manifest = options?.refreshManifest
|
|
447
|
-
? await refreshArtifactManifest(context)
|
|
591
|
+
? await refreshArtifactManifest(context, { sessionId })
|
|
448
592
|
: await readOrRefreshArtifactManifest(context);
|
|
449
|
-
const artifact = findArtifact(manifest.items,
|
|
593
|
+
const artifact = findArtifact(manifest.items, preparedSelector, { sessionId });
|
|
450
594
|
if (!artifact) {
|
|
451
595
|
throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, 'Artifact not found', {
|
|
452
596
|
artifactId: params.artifactId,
|
|
453
|
-
relativePath: params.relativePath
|
|
597
|
+
relativePath: params.relativePath,
|
|
598
|
+
sessionId
|
|
454
599
|
});
|
|
455
600
|
}
|
|
456
601
|
const checkedPath = ensureInside(manifest.workspacePaths.workspaceRoot, artifact.localPath);
|
|
@@ -464,19 +609,99 @@ async function resolveArtifact(params, context, options) {
|
|
|
464
609
|
localPath: checkedPath
|
|
465
610
|
};
|
|
466
611
|
}
|
|
467
|
-
function
|
|
612
|
+
async function prepareSessionArtifactSelector(params, context, sessionId) {
|
|
613
|
+
const relativePath = normalizeRelativePath(optionalTrimmedString(params.relativePath));
|
|
614
|
+
if (relativePath) {
|
|
615
|
+
const archivedRelativePath = await archiveArtifactFileToSessionDirectory(context, sessionId, relativePath);
|
|
616
|
+
return {
|
|
617
|
+
...params,
|
|
618
|
+
relativePath: archivedRelativePath ?? relativePath,
|
|
619
|
+
sessionId
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
const artifactId = optionalTrimmedString(params.artifactId);
|
|
623
|
+
if (!artifactId) {
|
|
624
|
+
return { ...params, sessionId };
|
|
625
|
+
}
|
|
626
|
+
const manifest = await readOrRefreshArtifactManifest(context);
|
|
627
|
+
const artifact = findArtifact(manifest.items, { artifactId });
|
|
628
|
+
if (!artifact || artifact.sessionId === sessionId) {
|
|
629
|
+
return { ...params, sessionId };
|
|
630
|
+
}
|
|
631
|
+
if (artifact.sessionId) {
|
|
632
|
+
return { ...params, sessionId };
|
|
633
|
+
}
|
|
634
|
+
const archivedRelativePath = await archiveArtifactFileToSessionDirectory(context, sessionId, artifact.relativePath);
|
|
635
|
+
return {
|
|
636
|
+
relativePath: archivedRelativePath ?? artifact.relativePath,
|
|
637
|
+
sessionId
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
async function archiveArtifactFileToSessionDirectory(context, sessionId, sourceRelativePath) {
|
|
641
|
+
const normalizedSourcePath = normalizeRelativePath(sourceRelativePath);
|
|
642
|
+
if (!normalizedSourcePath || extractSessionIdFromArtifactRelativePath(normalizedSourcePath)) {
|
|
643
|
+
return normalizedSourcePath;
|
|
644
|
+
}
|
|
645
|
+
const workspacePaths = await ensureWorkspacePaths(context.openclawRoot);
|
|
646
|
+
const sourceFullPath = ensureInside(workspacePaths.workspaceRoot, path.join(workspacePaths.workspaceRoot, normalizedSourcePath));
|
|
647
|
+
if (!(await pathExists(sourceFullPath))) {
|
|
648
|
+
return null;
|
|
649
|
+
}
|
|
650
|
+
const sourceStat = await fs.stat(sourceFullPath);
|
|
651
|
+
if (!sourceStat.isFile()) {
|
|
652
|
+
return null;
|
|
653
|
+
}
|
|
654
|
+
const existing = await readExistingManifest(workspacePaths.manifestPath);
|
|
655
|
+
const existingByPath = new Map(existing.map((item) => [item.relativePath, item]));
|
|
656
|
+
const archiveState = await readArtifactArchiveState(workspacePaths.artifactArchiveStatePath);
|
|
657
|
+
const archiveEntry = archiveState.files[normalizedSourcePath];
|
|
658
|
+
if (archiveEntry &&
|
|
659
|
+
archiveEntry.sessionId === sessionId &&
|
|
660
|
+
await artifactArchiveEntryIsCurrent(workspacePaths.workspaceRoot, archiveEntry, sourceStat)) {
|
|
661
|
+
return archiveEntry.targetRelativePath;
|
|
662
|
+
}
|
|
663
|
+
const targetRelativePath = await copyArtifactFileToSessionDirectory(workspacePaths.workspaceRoot, sessionId, normalizedSourcePath, sourceFullPath, sourceStat, existingByPath, archiveEntry);
|
|
664
|
+
archiveState.files[normalizedSourcePath] = {
|
|
665
|
+
sessionId,
|
|
666
|
+
targetRelativePath,
|
|
667
|
+
sizeBytes: sourceStat.size,
|
|
668
|
+
mtimeMs: sourceStat.mtimeMs,
|
|
669
|
+
archivedAt: new Date().toISOString()
|
|
670
|
+
};
|
|
671
|
+
await writeArtifactArchiveState(workspacePaths.artifactArchiveStatePath, archiveState);
|
|
672
|
+
return targetRelativePath;
|
|
673
|
+
}
|
|
674
|
+
function findArtifact(items, selector, options) {
|
|
468
675
|
const artifactId = optionalTrimmedString(selector.artifactId);
|
|
469
676
|
const relativePath = normalizeRelativePath(optionalTrimmedString(selector.relativePath));
|
|
677
|
+
const sessionId = optionalTrimmedString(options?.sessionId);
|
|
470
678
|
if (!artifactId && !relativePath) {
|
|
471
679
|
throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, 'Either artifactId or relativePath is required');
|
|
472
680
|
}
|
|
473
681
|
return items.find((item) => {
|
|
682
|
+
if (sessionId && item.sessionId !== sessionId) {
|
|
683
|
+
return false;
|
|
684
|
+
}
|
|
474
685
|
if (artifactId && item.id === artifactId) {
|
|
475
686
|
return true;
|
|
476
687
|
}
|
|
477
|
-
return Boolean(relativePath && item
|
|
688
|
+
return Boolean(relativePath && artifactRelativePathMatches(item, relativePath, sessionId));
|
|
478
689
|
}) ?? null;
|
|
479
690
|
}
|
|
691
|
+
function artifactRelativePathMatches(item, relativePath, sessionId) {
|
|
692
|
+
if (item.relativePath === relativePath) {
|
|
693
|
+
return true;
|
|
694
|
+
}
|
|
695
|
+
if (!sessionId) {
|
|
696
|
+
return false;
|
|
697
|
+
}
|
|
698
|
+
const sessionRelativePath = buildSessionArtifactRelativePath(sessionId, relativePath);
|
|
699
|
+
return item.relativePath === sessionRelativePath;
|
|
700
|
+
}
|
|
701
|
+
function buildSessionArtifactRelativePath(sessionId, relativePath) {
|
|
702
|
+
const normalizedRelativePath = normalizeRelativePath(relativePath) ?? path.posix.basename(relativePath);
|
|
703
|
+
return `${SESSION_ARTIFACTS_ROOT_RELATIVE}/${safeSessionDirectoryName(sessionId)}/${normalizedRelativePath}`;
|
|
704
|
+
}
|
|
480
705
|
function resolveWorkspaceRoot(openclawRoot) {
|
|
481
706
|
const workspaceRoot = path.join(openclawRoot, 'workspace');
|
|
482
707
|
return ensureInside(openclawRoot, workspaceRoot);
|
|
@@ -485,11 +710,13 @@ async function ensureWorkspacePaths(openclawRoot) {
|
|
|
485
710
|
const workspaceRoot = resolveWorkspaceRoot(openclawRoot);
|
|
486
711
|
const manifestPath = ensureInside(workspaceRoot, path.join(workspaceRoot, ARTIFACT_MANIFEST_FILE));
|
|
487
712
|
const markdownScanStatePath = ensureInside(workspaceRoot, path.join(workspaceRoot, PLUGIN_WORKSPACE_STATE_DIR, MARKDOWN_SCAN_STATE_FILE));
|
|
713
|
+
const artifactArchiveStatePath = ensureInside(workspaceRoot, path.join(workspaceRoot, PLUGIN_WORKSPACE_STATE_DIR, ARTIFACT_ARCHIVE_STATE_FILE));
|
|
488
714
|
await ensureDir(workspaceRoot);
|
|
489
715
|
return {
|
|
490
716
|
workspaceRoot,
|
|
491
717
|
manifestPath,
|
|
492
|
-
markdownScanStatePath
|
|
718
|
+
markdownScanStatePath,
|
|
719
|
+
artifactArchiveStatePath
|
|
493
720
|
};
|
|
494
721
|
}
|
|
495
722
|
function createMarkdownScanState() {
|
|
@@ -500,9 +727,94 @@ function createMarkdownScanState() {
|
|
|
500
727
|
files: {}
|
|
501
728
|
};
|
|
502
729
|
}
|
|
730
|
+
function createArtifactArchiveState() {
|
|
731
|
+
return {
|
|
732
|
+
version: 1,
|
|
733
|
+
files: {}
|
|
734
|
+
};
|
|
735
|
+
}
|
|
503
736
|
function buildArtifactId(relativePath) {
|
|
504
737
|
return `art_${createHash('sha1').update(relativePath).digest('hex').slice(0, 12)}`;
|
|
505
738
|
}
|
|
739
|
+
async function resolveCanonicalArtifactSessionId(context, sessionIdOrKey) {
|
|
740
|
+
if (!sessionIdOrKey) {
|
|
741
|
+
return null;
|
|
742
|
+
}
|
|
743
|
+
const session = await findSessionRecord(context, sessionIdOrKey).catch(() => null);
|
|
744
|
+
if (session?.sessionId) {
|
|
745
|
+
return session.sessionId;
|
|
746
|
+
}
|
|
747
|
+
if (looksLikeOpenClawSessionKey(sessionIdOrKey)) {
|
|
748
|
+
throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, `Session key not found: ${sessionIdOrKey}`, { sessionKey: sessionIdOrKey });
|
|
749
|
+
}
|
|
750
|
+
return sessionIdOrKey;
|
|
751
|
+
}
|
|
752
|
+
async function resolveArchiveStateSessionId(context, sessionIdOrKey) {
|
|
753
|
+
try {
|
|
754
|
+
return await resolveCanonicalArtifactSessionId(context, sessionIdOrKey);
|
|
755
|
+
}
|
|
756
|
+
catch (error) {
|
|
757
|
+
if (looksLikeOpenClawSessionKey(sessionIdOrKey)) {
|
|
758
|
+
return null;
|
|
759
|
+
}
|
|
760
|
+
throw error;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
function looksLikeOpenClawSessionKey(value) {
|
|
764
|
+
return value.trim().toLowerCase().startsWith('agent:');
|
|
765
|
+
}
|
|
766
|
+
async function resolveAvailableSessionArtifactRelativePath(workspaceRoot, sessionId, sourceRelativePath, existingByPath) {
|
|
767
|
+
const normalizedSourcePath = normalizeRelativePath(sourceRelativePath) ?? path.basename(sourceRelativePath);
|
|
768
|
+
const sessionRoot = `${SESSION_ARTIFACTS_ROOT_RELATIVE}/${safeSessionDirectoryName(sessionId)}`;
|
|
769
|
+
const initialTarget = `${sessionRoot}/${normalizedSourcePath}`;
|
|
770
|
+
const parsed = path.posix.parse(initialTarget);
|
|
771
|
+
let candidate = initialTarget;
|
|
772
|
+
let suffix = 1;
|
|
773
|
+
while (existingByPath.has(candidate) ||
|
|
774
|
+
await pathExists(ensureInside(workspaceRoot, path.join(workspaceRoot, candidate)))) {
|
|
775
|
+
candidate = path.posix.join(parsed.dir, `${parsed.name}-${suffix}${parsed.ext}`);
|
|
776
|
+
suffix += 1;
|
|
777
|
+
}
|
|
778
|
+
return candidate;
|
|
779
|
+
}
|
|
780
|
+
function safeSessionDirectoryName(sessionId) {
|
|
781
|
+
const trimmed = sessionId.trim();
|
|
782
|
+
const sanitized = trimmed
|
|
783
|
+
.replace(/[<>:"/\\|?*\x00-\x1F]/g, '_')
|
|
784
|
+
.replace(/^\.+$/, '_')
|
|
785
|
+
.replace(/[. ]+$/g, '')
|
|
786
|
+
.slice(0, 120);
|
|
787
|
+
if (sanitized) {
|
|
788
|
+
return sanitized;
|
|
789
|
+
}
|
|
790
|
+
return createHash('sha1').update(trimmed).digest('hex').slice(0, 12);
|
|
791
|
+
}
|
|
792
|
+
function extractSessionIdFromArtifactRelativePath(relativePath) {
|
|
793
|
+
const normalized = normalizeRelativePath(relativePath);
|
|
794
|
+
if (!normalized) {
|
|
795
|
+
return null;
|
|
796
|
+
}
|
|
797
|
+
const prefix = `${SESSION_ARTIFACTS_ROOT_RELATIVE}/`;
|
|
798
|
+
if (!normalized.startsWith(prefix)) {
|
|
799
|
+
return null;
|
|
800
|
+
}
|
|
801
|
+
const rest = normalized.slice(prefix.length);
|
|
802
|
+
const sessionId = rest.split('/')[0]?.trim();
|
|
803
|
+
return sessionId || null;
|
|
804
|
+
}
|
|
805
|
+
function resolveArtifactSessionId(relativePath, existingItem, taggedSessionId) {
|
|
806
|
+
if (existingItem) {
|
|
807
|
+
return existingItem.sessionId ?? null;
|
|
808
|
+
}
|
|
809
|
+
const pathSessionId = extractSessionIdFromArtifactRelativePath(relativePath);
|
|
810
|
+
if (!pathSessionId) {
|
|
811
|
+
return taggedSessionId ?? null;
|
|
812
|
+
}
|
|
813
|
+
if (taggedSessionId && pathSessionId === safeSessionDirectoryName(taggedSessionId)) {
|
|
814
|
+
return taggedSessionId;
|
|
815
|
+
}
|
|
816
|
+
return pathSessionId;
|
|
817
|
+
}
|
|
506
818
|
function normalizeExtension(fileName) {
|
|
507
819
|
const ext = path.extname(fileName).toLowerCase();
|
|
508
820
|
return ext || null;
|
|
@@ -531,7 +843,12 @@ function normalizeMaxInlineBytes(value) {
|
|
|
531
843
|
function isPreviewableCategory(category) {
|
|
532
844
|
return category === 'image' || category === 'video' || category === 'document';
|
|
533
845
|
}
|
|
534
|
-
function shouldIgnoreArtifactDirectory(dirName) {
|
|
846
|
+
function shouldIgnoreArtifactDirectory(dirName, relativePath) {
|
|
847
|
+
const normalizedRelativePath = relativePath?.replace(/\\/g, '/').toLowerCase();
|
|
848
|
+
if (normalizedRelativePath === SESSION_ARTIFACTS_ROOT_RELATIVE ||
|
|
849
|
+
normalizedRelativePath?.startsWith(`${SESSION_ARTIFACTS_ROOT_RELATIVE}/`)) {
|
|
850
|
+
return false;
|
|
851
|
+
}
|
|
535
852
|
const normalized = dirName.trim().toLowerCase();
|
|
536
853
|
return !normalized || normalized.startsWith('.') || IGNORE_DIRECTORY_NAMES.has(normalized);
|
|
537
854
|
}
|
|
@@ -541,7 +858,7 @@ function shouldIncludeArtifactFile(fileName) {
|
|
|
541
858
|
}
|
|
542
859
|
return classifyArtifactCategory(fileName) !== 'other';
|
|
543
860
|
}
|
|
544
|
-
function shouldIncludeMarkdownArtifactFile(relativePath, fileName, stat, state, hadState) {
|
|
861
|
+
function shouldIncludeMarkdownArtifactFile(relativePath, fileName, stat, state, hadState, includeNewFile) {
|
|
545
862
|
if (shouldIgnoreArtifactFile(fileName)) {
|
|
546
863
|
return false;
|
|
547
864
|
}
|
|
@@ -558,9 +875,9 @@ function shouldIncludeMarkdownArtifactFile(relativePath, fileName, stat, state,
|
|
|
558
875
|
sizeBytes: stat.size,
|
|
559
876
|
mtimeMs: stat.mtimeMs,
|
|
560
877
|
firstSeenAt: new Date().toISOString(),
|
|
561
|
-
baseline: !hadState
|
|
878
|
+
baseline: !hadState && !includeNewFile
|
|
562
879
|
};
|
|
563
|
-
return hadState;
|
|
880
|
+
return hadState || includeNewFile;
|
|
564
881
|
}
|
|
565
882
|
function isMarkdownScanStateFile(value) {
|
|
566
883
|
if (!value || Array.isArray(value) || typeof value !== 'object') {
|
|
@@ -572,6 +889,17 @@ function isMarkdownScanStateFile(value) {
|
|
|
572
889
|
typeof objectValue.firstSeenAt === 'string' &&
|
|
573
890
|
typeof objectValue.baseline === 'boolean');
|
|
574
891
|
}
|
|
892
|
+
function isArtifactArchiveStateFile(value) {
|
|
893
|
+
if (!value || Array.isArray(value) || typeof value !== 'object') {
|
|
894
|
+
return false;
|
|
895
|
+
}
|
|
896
|
+
const objectValue = value;
|
|
897
|
+
return (typeof objectValue.sessionId === 'string' &&
|
|
898
|
+
typeof objectValue.targetRelativePath === 'string' &&
|
|
899
|
+
typeof objectValue.sizeBytes === 'number' &&
|
|
900
|
+
typeof objectValue.mtimeMs === 'number' &&
|
|
901
|
+
typeof objectValue.archivedAt === 'string');
|
|
902
|
+
}
|
|
575
903
|
function isArtifactRecord(value) {
|
|
576
904
|
if (!value || Array.isArray(value) || typeof value !== 'object') {
|
|
577
905
|
return false;
|
|
@@ -594,6 +922,7 @@ function artifactToJsonValue(item) {
|
|
|
594
922
|
sizeBytes: item.sizeBytes,
|
|
595
923
|
storageStatus: item.storageStatus,
|
|
596
924
|
source: item.source,
|
|
925
|
+
sessionId: item.sessionId ?? null,
|
|
597
926
|
previewable: item.previewable,
|
|
598
927
|
createdAt: item.createdAt,
|
|
599
928
|
updatedAt: item.updatedAt,
|
|
@@ -631,13 +960,20 @@ function expectString(value, fieldName) {
|
|
|
631
960
|
function optionalTrimmedString(value) {
|
|
632
961
|
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;
|
|
633
962
|
}
|
|
634
|
-
function
|
|
963
|
+
function normalizeArtifactFilters(params) {
|
|
635
964
|
return {
|
|
965
|
+
sessionId: optionalTrimmedString(params.sessionId),
|
|
966
|
+
query: normalizeOptionalLowercaseString(params.query),
|
|
636
967
|
category: normalizeArtifactCategory(optionalTrimmedString(params.category)),
|
|
637
968
|
mimeType: normalizeOptionalLowercaseString(params.mimeType),
|
|
638
969
|
fileName: normalizeOptionalLowercaseString(params.fileName),
|
|
639
970
|
relativePathPrefix: normalizeRelativePath(optionalTrimmedString(params.relativePathPrefix)),
|
|
640
|
-
createdAfterMs: normalizeOptionalTimestamp(params.createdAfter)
|
|
971
|
+
createdAfterMs: normalizeOptionalTimestamp(params.createdAfter)
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
function normalizeArtifactFindFilters(params) {
|
|
975
|
+
return {
|
|
976
|
+
...normalizeArtifactFilters(params),
|
|
641
977
|
limit: normalizeArtifactLimit(params.limit)
|
|
642
978
|
};
|
|
643
979
|
}
|
|
@@ -680,14 +1016,36 @@ function normalizeArtifactLimit(value) {
|
|
|
680
1016
|
}
|
|
681
1017
|
return Math.min(50, Math.floor(value));
|
|
682
1018
|
}
|
|
683
|
-
function
|
|
1019
|
+
function normalizeArtifactGroupBy(value) {
|
|
1020
|
+
const normalized = normalizeOptionalLowercaseString(value);
|
|
1021
|
+
if (!normalized || normalized === 'none') {
|
|
1022
|
+
return null;
|
|
1023
|
+
}
|
|
1024
|
+
if (normalized === 'session') {
|
|
1025
|
+
return 'session';
|
|
1026
|
+
}
|
|
1027
|
+
if (normalized === 'category' || normalized === 'type') {
|
|
1028
|
+
return 'category';
|
|
1029
|
+
}
|
|
1030
|
+
if (normalized === 'day' || normalized === 'date' || normalized === 'time') {
|
|
1031
|
+
return 'day';
|
|
1032
|
+
}
|
|
1033
|
+
throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, `Unsupported artifact groupBy: ${normalized}`);
|
|
1034
|
+
}
|
|
1035
|
+
function matchesArtifactFilters(item, filters) {
|
|
1036
|
+
if (filters.sessionId && item.sessionId !== filters.sessionId) {
|
|
1037
|
+
return false;
|
|
1038
|
+
}
|
|
1039
|
+
if (filters.query && !artifactMatchesQuery(item, filters.query)) {
|
|
1040
|
+
return false;
|
|
1041
|
+
}
|
|
684
1042
|
if (filters.category && item.category !== filters.category) {
|
|
685
1043
|
return false;
|
|
686
1044
|
}
|
|
687
1045
|
if (filters.mimeType && item.mimeType.toLowerCase() !== filters.mimeType) {
|
|
688
1046
|
return false;
|
|
689
1047
|
}
|
|
690
|
-
if (filters.fileName && item.fileName.toLowerCase()
|
|
1048
|
+
if (filters.fileName && !item.fileName.toLowerCase().includes(filters.fileName)) {
|
|
691
1049
|
return false;
|
|
692
1050
|
}
|
|
693
1051
|
if (filters.relativePathPrefix && !item.relativePath.startsWith(filters.relativePathPrefix)) {
|
|
@@ -698,6 +1056,84 @@ function matchesArtifactFindFilters(item, filters) {
|
|
|
698
1056
|
}
|
|
699
1057
|
return true;
|
|
700
1058
|
}
|
|
1059
|
+
function matchesArtifactFindFilters(item, filters) {
|
|
1060
|
+
return matchesArtifactFilters(item, filters);
|
|
1061
|
+
}
|
|
1062
|
+
function artifactMatchesQuery(item, query) {
|
|
1063
|
+
const values = [
|
|
1064
|
+
item.fileName,
|
|
1065
|
+
item.relativePath,
|
|
1066
|
+
item.category,
|
|
1067
|
+
item.mimeType,
|
|
1068
|
+
item.ext ?? '',
|
|
1069
|
+
item.sessionId ?? ''
|
|
1070
|
+
];
|
|
1071
|
+
return values.some((value) => value.toLowerCase().includes(query));
|
|
1072
|
+
}
|
|
1073
|
+
function buildArtifactGroups(items, groupBy) {
|
|
1074
|
+
const groups = new Map();
|
|
1075
|
+
for (const item of items) {
|
|
1076
|
+
const { key, label } = resolveArtifactGroup(item, groupBy);
|
|
1077
|
+
const group = groups.get(key) ?? { key, label, items: [] };
|
|
1078
|
+
group.items.push(item);
|
|
1079
|
+
groups.set(key, group);
|
|
1080
|
+
}
|
|
1081
|
+
return Array.from(groups.values())
|
|
1082
|
+
.sort((left, right) => compareArtifactGroups(left, right, groupBy))
|
|
1083
|
+
.map((group) => ({
|
|
1084
|
+
key: group.key,
|
|
1085
|
+
label: group.label,
|
|
1086
|
+
count: group.items.length,
|
|
1087
|
+
items: group.items.map(artifactToJsonValue)
|
|
1088
|
+
}));
|
|
1089
|
+
}
|
|
1090
|
+
function resolveArtifactGroup(item, groupBy) {
|
|
1091
|
+
if (groupBy === 'session') {
|
|
1092
|
+
const sessionId = item.sessionId ?? null;
|
|
1093
|
+
return {
|
|
1094
|
+
key: sessionId ?? 'unassigned',
|
|
1095
|
+
label: sessionId ?? 'Unassigned'
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
if (groupBy === 'category') {
|
|
1099
|
+
return {
|
|
1100
|
+
key: item.category,
|
|
1101
|
+
label: item.category
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
const timestamp = Date.parse(item.createdAt);
|
|
1105
|
+
if (Number.isNaN(timestamp)) {
|
|
1106
|
+
return {
|
|
1107
|
+
key: 'unknown',
|
|
1108
|
+
label: 'Unknown'
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
const day = new Date(timestamp).toISOString().slice(0, 10);
|
|
1112
|
+
return {
|
|
1113
|
+
key: day,
|
|
1114
|
+
label: day
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
function compareArtifactGroups(left, right, groupBy) {
|
|
1118
|
+
if (groupBy === 'day') {
|
|
1119
|
+
return right.key.localeCompare(left.key);
|
|
1120
|
+
}
|
|
1121
|
+
if (groupBy === 'category') {
|
|
1122
|
+
return artifactCategorySortIndex(left.key) - artifactCategorySortIndex(right.key);
|
|
1123
|
+
}
|
|
1124
|
+
if (left.key === 'unassigned') {
|
|
1125
|
+
return 1;
|
|
1126
|
+
}
|
|
1127
|
+
if (right.key === 'unassigned') {
|
|
1128
|
+
return -1;
|
|
1129
|
+
}
|
|
1130
|
+
return left.key.localeCompare(right.key);
|
|
1131
|
+
}
|
|
1132
|
+
function artifactCategorySortIndex(value) {
|
|
1133
|
+
const order = ['image', 'video', 'document', 'archive', 'other'];
|
|
1134
|
+
const index = order.indexOf(value);
|
|
1135
|
+
return index === -1 ? order.length : index;
|
|
1136
|
+
}
|
|
701
1137
|
function compareArtifactsByRecency(left, right) {
|
|
702
1138
|
const createdDelta = Date.parse(right.createdAt) - Date.parse(left.createdAt);
|
|
703
1139
|
if (createdDelta !== 0) {
|