rol-websocket-channel 1.4.2 → 1.4.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.
Files changed (43) hide show
  1. package/{MQTT-API /346/226/260/345/242/236/346/226/207/344/273/266/345/212/237/350/203/275.md" → MQTT-API 5-6.md } +89 -1
  2. package/dist/index.js +617 -617
  3. package/dist/message-handler.js +515 -503
  4. package/dist/src/admin/cli.js +43 -43
  5. package/dist/src/admin/jsonrpc.js +60 -60
  6. package/dist/src/admin/lib/fs.js +30 -30
  7. package/dist/src/admin/lib/paths.js +80 -80
  8. package/dist/src/admin/methods/admin.js +60 -60
  9. package/dist/src/admin/methods/agents-extended.js +251 -251
  10. package/dist/src/admin/methods/artifacts.js +736 -642
  11. package/dist/src/admin/methods/artifacts.test.js +210 -191
  12. package/dist/src/admin/methods/cron.js +250 -250
  13. package/dist/src/admin/methods/index.js +104 -102
  14. package/dist/src/admin/methods/mem9.js +309 -270
  15. package/dist/src/admin/methods/mem9.test.js +34 -0
  16. package/dist/src/admin/methods/memory.js +363 -363
  17. package/dist/src/admin/methods/models-extended.js +190 -190
  18. package/dist/src/admin/methods/models.js +195 -195
  19. package/dist/src/admin/methods/pairing.js +268 -268
  20. package/dist/src/admin/methods/sessions-extended.js +215 -215
  21. package/dist/src/admin/methods/sessions.js +75 -75
  22. package/dist/src/admin/methods/skills-extended.js +157 -157
  23. package/dist/src/admin/methods/skills-toggle.js +183 -183
  24. package/dist/src/admin/methods/skills.js +528 -528
  25. package/dist/src/admin/methods/system.js +271 -180
  26. package/dist/src/admin/methods/usage.js +1170 -1170
  27. package/dist/src/admin/types.js +1 -1
  28. package/dist/src/mqtt/connection-manager.js +209 -209
  29. package/dist/src/mqtt/index.js +5 -5
  30. package/dist/src/mqtt/mqtt-client.js +110 -110
  31. package/dist/src/mqtt/mqtt.test.js +418 -418
  32. package/dist/src/mqtt/types.js +2 -2
  33. package/dist/src/shared/context.js +24 -24
  34. package/dist/src/shared/wrapper.js +23 -23
  35. package/message-handler.ts +15 -1
  36. package/openclaw.plugin.json +73 -0
  37. package/package.json +1 -1
  38. package/src/admin/methods/artifacts.test.ts +35 -0
  39. package/src/admin/methods/artifacts.ts +140 -2
  40. package/src/admin/methods/index.ts +3 -1
  41. package/src/admin/methods/mem9.test.ts +39 -0
  42. package/src/admin/methods/mem9.ts +48 -1
  43. package/src/admin/methods/system.ts +129 -1
@@ -1,642 +1,736 @@
1
- import { createHash } from 'node:crypto';
2
- import fs from 'node:fs/promises';
3
- import path from 'node:path';
4
- import { ensureDir, pathExists, readJsonFile } from '../lib/fs.js';
5
- import { ensureInside } from '../lib/paths.js';
6
- import { JsonRpcException, JSON_RPC_ERRORS } from '../jsonrpc.js';
7
- const INLINE_CONTENT_LIMIT_BYTES = 10 * 1024 * 1024;
8
- const MIN_ARTIFACT_SIZE_BYTES = 1;
9
- const ARTIFACT_MANIFEST_FILE = 'artifacts.json';
10
- const IGNORE_EXTENSIONS = new Set(['.tmp', '.part', '.crdownload']);
11
- const IGNORE_FILE_NAMES = new Set(['.ds_store', 'thumbs.db', ARTIFACT_MANIFEST_FILE]);
12
- const IGNORE_DIRECTORY_NAMES = new Set([
13
- '.git',
14
- '.cache',
15
- 'cache',
16
- 'caches',
17
- 'tmp',
18
- 'temp',
19
- 'logs',
20
- 'log',
21
- 'node_modules',
22
- 'sessions',
23
- 'session',
24
- 'history'
25
- ]);
26
- const CATEGORY_BY_EXTENSION = {
27
- '.png': 'image',
28
- '.jpg': 'image',
29
- '.jpeg': 'image',
30
- '.webp': 'image',
31
- '.gif': 'image',
32
- '.svg': 'image',
33
- '.mp4': 'video',
34
- '.mov': 'video',
35
- '.webm': 'video',
36
- '.avi': 'video',
37
- '.mkv': 'video',
38
- '.m4v': 'video',
39
- '.pdf': 'document',
40
- '.doc': 'document',
41
- '.docx': 'document',
42
- '.zip': 'archive',
43
- '.7z': 'archive',
44
- '.tar': 'archive',
45
- '.gz': 'archive',
46
- '.rar': 'archive'
47
- };
48
- const MIME_BY_EXTENSION = {
49
- '.png': 'image/png',
50
- '.jpg': 'image/jpeg',
51
- '.jpeg': 'image/jpeg',
52
- '.webp': 'image/webp',
53
- '.gif': 'image/gif',
54
- '.svg': 'image/svg+xml',
55
- '.mp4': 'video/mp4',
56
- '.mov': 'video/quicktime',
57
- '.webm': 'video/webm',
58
- '.avi': 'video/x-msvideo',
59
- '.mkv': 'video/x-matroska',
60
- '.m4v': 'video/x-m4v',
61
- '.pdf': 'application/pdf',
62
- '.doc': 'application/msword',
63
- '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
64
- '.zip': 'application/zip',
65
- '.7z': 'application/x-7z-compressed',
66
- '.tar': 'application/x-tar',
67
- '.gz': 'application/gzip',
68
- '.rar': 'application/vnd.rar'
69
- };
70
- export const listArtifacts = async (params, context) => {
71
- const objectParams = expectOptionalObject(params);
72
- const refresh = objectParams.refresh !== false;
73
- const manifest = refresh
74
- ? await refreshArtifactManifest(context)
75
- : await readOrRefreshArtifactManifest(context);
76
- return {
77
- scope: 'workspace',
78
- count: manifest.items.length,
79
- manifestPath: manifest.manifestPath,
80
- workspaceRoot: manifest.workspacePaths.workspaceRoot,
81
- items: manifest.items.map(artifactToJsonValue)
82
- };
83
- };
84
- export const refreshArtifacts = async (params, context) => {
85
- const objectParams = expectOptionalObject(params);
86
- const manifest = await refreshArtifactManifest(context);
87
- return {
88
- ok: true,
89
- scope: 'workspace',
90
- count: manifest.items.length,
91
- manifestPath: manifest.manifestPath,
92
- workspaceRoot: manifest.workspacePaths.workspaceRoot,
93
- items: manifest.items.map(artifactToJsonValue)
94
- };
95
- };
96
- export const getArtifactContent = async (params, context) => {
97
- const objectParams = expectObject(params);
98
- const maxInlineBytes = normalizeMaxInlineBytes(objectParams.maxInlineBytes);
99
- const artifact = await resolveArtifact(objectParams, context);
100
- if (artifact.sizeBytes > maxInlineBytes) {
101
- throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, 'Artifact is too large for inline transfer', {
102
- artifactId: artifact.id,
103
- sizeBytes: artifact.sizeBytes,
104
- maxInlineBytes
105
- });
106
- }
107
- const content = await fs.readFile(artifact.localPath);
108
- return {
109
- scope: 'workspace',
110
- artifact: {
111
- ...artifactToJsonValue(artifact),
112
- contentBase64: content.toString('base64')
113
- }
114
- };
115
- };
116
- export const ensureArtifactUploaded = async (params, context) => {
117
- const objectParams = expectObject(params);
118
- const artifact = await resolveArtifact(objectParams, context);
119
- if (artifact.storageStatus === 'uploaded' && artifact.fileUrl) {
120
- return {
121
- ok: true,
122
- scope: 'workspace',
123
- uploaded: false,
124
- artifactId: artifact.id,
125
- objectKey: artifact.objectKey ?? null,
126
- downloadUrl: artifact.fileUrl,
127
- item: artifactToJsonValue(artifact)
128
- };
129
- }
130
- const endpointConfig = await resolveApiCoreBotEndpoint(context.openclawRoot, objectParams.baseUrl, objectParams.authToken);
131
- const presignedPostBody = buildPresignedPostBody(objectParams.presignedPostBody, artifact);
132
- const presignedResponse = await postJson(`${endpointConfig.baseUrl}/api-core-bot/front/s3/get-presigned-post`, presignedPostBody, endpointConfig.authToken);
133
- const uploadTarget = resolvePresignedPostUploadTarget(presignedResponse);
134
- await uploadArtifactToPresignedPost(uploadTarget, artifact);
135
- const objectKey = uploadTarget.objectKey ?? artifact.objectKey ?? null;
136
- const fileUrl = uploadTarget.fileUrl ?? artifact.fileUrl ?? buildPublicFileUrl(uploadTarget.uploadUrl, objectKey);
137
- if (!fileUrl) {
138
- throw new JsonRpcException(JSON_RPC_ERRORS.internalError, 'Unable to determine uploaded artifact download URL', {
139
- artifactId: artifact.id,
140
- objectKey
141
- });
142
- }
143
- const manifest = await readOrRefreshArtifactManifest(context);
144
- const updated = await persistUploadedArtifact(manifest.manifestPath, manifest.items, artifact.id, {
145
- objectKey,
146
- fileUrl,
147
- record: artifact.record
148
- });
149
- return {
150
- ok: true,
151
- scope: 'workspace',
152
- uploaded: true,
153
- artifactId: artifact.id,
154
- objectKey,
155
- downloadUrl: fileUrl,
156
- item: artifactToJsonValue(updated)
157
- };
158
- };
159
- export const getArtifactPresignedPost = async (params, context) => {
160
- const objectParams = expectObject(params);
161
- const body = isObject(objectParams.body) ? objectParams.body : null;
162
- if (!body) {
163
- throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, 'Missing required parameter: body');
164
- }
165
- const endpointConfig = await resolveApiCoreBotEndpoint(context.openclawRoot, objectParams.baseUrl, objectParams.authToken);
166
- const response = await postJson(`${endpointConfig.baseUrl}/api-core-bot/front/s3/get-presigned-post`, body, endpointConfig.authToken);
167
- return {
168
- ok: true,
169
- endpoint: '/api-core-bot/front/s3/get-presigned-post',
170
- data: response
171
- };
172
- };
173
- export const createArtifactRecord = async (params, context) => {
174
- const objectParams = expectObject(params);
175
- const body = isObject(objectParams.body) ? objectParams.body : null;
176
- if (!body) {
177
- throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, 'Missing required parameter: body');
178
- }
179
- const endpoint = expectString(objectParams.endpoint, 'endpoint');
180
- if (!endpoint.startsWith('/api-core-bot/front/')) {
181
- throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, 'endpoint must start with /api-core-bot/front/');
182
- }
183
- const endpointConfig = await resolveApiCoreBotEndpoint(context.openclawRoot, objectParams.baseUrl, objectParams.authToken);
184
- const response = await postJson(`${endpointConfig.baseUrl}${endpoint}`, body, endpointConfig.authToken);
185
- return {
186
- ok: true,
187
- endpoint,
188
- data: response
189
- };
190
- };
191
- export const markArtifactUploaded = async (params, context) => {
192
- const objectParams = expectObject(params);
193
- const objectKey = optionalTrimmedString(objectParams.objectKey);
194
- const fileUrl = optionalTrimmedString(objectParams.fileUrl);
195
- if (!objectKey && !fileUrl) {
196
- throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, 'At least one of objectKey or fileUrl is required');
197
- }
198
- const manifest = await readOrRefreshArtifactManifest(context);
199
- const artifact = findArtifact(manifest.items, objectParams);
200
- if (!artifact) {
201
- throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, 'Artifact not found', {
202
- artifactId: objectParams.artifactId,
203
- relativePath: objectParams.relativePath
204
- });
205
- }
206
- const updated = await persistUploadedArtifact(manifest.manifestPath, manifest.items, artifact.id, {
207
- objectKey: objectKey ?? artifact.objectKey ?? null,
208
- fileUrl: fileUrl ?? artifact.fileUrl ?? null,
209
- record: objectParams.record === undefined ? artifact.record : objectParams.record
210
- });
211
- return {
212
- ok: true,
213
- scope: 'workspace',
214
- artifactId: artifact.id,
215
- item: artifactToJsonValue(updated)
216
- };
217
- };
218
- export function classifyArtifactCategory(fileName) {
219
- const ext = path.extname(fileName).toLowerCase();
220
- return CATEGORY_BY_EXTENSION[ext] ?? 'other';
221
- }
222
- export function shouldIgnoreArtifactFile(fileName) {
223
- const normalized = fileName.trim().toLowerCase();
224
- if (!normalized) {
225
- return true;
226
- }
227
- if (normalized.startsWith('.')) {
228
- return true;
229
- }
230
- if (IGNORE_FILE_NAMES.has(normalized)) {
231
- return true;
232
- }
233
- const ext = path.extname(normalized);
234
- return IGNORE_EXTENSIONS.has(ext);
235
- }
236
- async function refreshArtifactManifest(context) {
237
- const workspacePaths = await ensureWorkspacePaths(context.openclawRoot);
238
- const { workspaceRoot, manifestPath } = workspacePaths;
239
- const existing = await readExistingManifest(manifestPath);
240
- const existingByPath = new Map(existing.map((item) => [item.relativePath, item]));
241
- const files = await collectArtifactFiles(workspaceRoot);
242
- const items = [];
243
- for (const fullPath of files) {
244
- const stat = await fs.stat(fullPath);
245
- if (!stat.isFile() || stat.size < MIN_ARTIFACT_SIZE_BYTES) {
246
- continue;
247
- }
248
- const relativePath = path.relative(workspaceRoot, fullPath).replace(/\\/g, '/');
249
- const existingItem = existingByPath.get(relativePath);
250
- const fileName = path.basename(fullPath);
251
- if (!shouldIncludeArtifactFile(fileName)) {
252
- continue;
253
- }
254
- const ext = normalizeExtension(fileName);
255
- const createdAt = normalizeTimestamp(stat.birthtime, stat.mtime);
256
- const updatedAt = stat.mtime.toISOString();
257
- items.push({
258
- id: existingItem?.id ?? buildArtifactId(relativePath),
259
- fileName,
260
- relativePath,
261
- localPath: fullPath,
262
- category: classifyArtifactCategory(fileName),
263
- mimeType: MIME_BY_EXTENSION[ext ?? ''] ?? 'application/octet-stream',
264
- ext,
265
- sizeBytes: stat.size,
266
- storageStatus: existingItem?.storageStatus ?? 'local_only',
267
- source: existingItem?.source ?? 'generated',
268
- previewable: isPreviewableCategory(classifyArtifactCategory(fileName)),
269
- createdAt: existingItem?.createdAt ?? createdAt,
270
- updatedAt,
271
- objectKey: existingItem?.objectKey ?? null,
272
- fileUrl: existingItem?.fileUrl ?? null,
273
- uploadedAt: existingItem?.uploadedAt ?? null,
274
- record: existingItem?.record
275
- });
276
- }
277
- items.sort((a, b) => a.fileName.localeCompare(b.fileName));
278
- await writeArtifactManifest(manifestPath, items);
279
- return { manifestPath, items, workspacePaths };
280
- }
281
- async function readOrRefreshArtifactManifest(context) {
282
- const workspacePaths = await ensureWorkspacePaths(context.openclawRoot);
283
- const { manifestPath } = workspacePaths;
284
- if (!(await pathExists(manifestPath))) {
285
- return await refreshArtifactManifest(context);
286
- }
287
- const items = await readExistingManifest(manifestPath);
288
- return { manifestPath, items, workspacePaths };
289
- }
290
- async function readExistingManifest(manifestPath) {
291
- if (!(await pathExists(manifestPath))) {
292
- return [];
293
- }
294
- const parsed = await readJsonFile(manifestPath);
295
- if (!Array.isArray(parsed)) {
296
- return [];
297
- }
298
- const items = [];
299
- for (const item of parsed) {
300
- if (isArtifactRecord(item)) {
301
- items.push(item);
302
- }
303
- }
304
- return items;
305
- }
306
- async function writeArtifactManifest(manifestPath, items) {
307
- await fs.writeFile(manifestPath, JSON.stringify(items.map((item) => artifactToJsonValue(item)), null, 2), 'utf8');
308
- }
309
- async function persistUploadedArtifact(manifestPath, items, artifactId, updates) {
310
- const now = new Date().toISOString();
311
- let updatedArtifact = null;
312
- const updatedItems = items.map((item) => {
313
- if (!isArtifactRecord(item) || item.id !== artifactId) {
314
- return item;
315
- }
316
- updatedArtifact = {
317
- ...item,
318
- storageStatus: 'uploaded',
319
- objectKey: updates.objectKey ?? item.objectKey ?? null,
320
- fileUrl: updates.fileUrl ?? item.fileUrl ?? null,
321
- uploadedAt: now,
322
- updatedAt: now,
323
- ...(updates.record === undefined ? {} : { record: updates.record })
324
- };
325
- return updatedArtifact;
326
- });
327
- await writeArtifactManifest(manifestPath, updatedItems);
328
- if (!updatedArtifact) {
329
- throw new JsonRpcException(JSON_RPC_ERRORS.internalError, 'Artifact not found while persisting upload state', {
330
- artifactId
331
- });
332
- }
333
- return updatedArtifact;
334
- }
335
- async function collectArtifactFiles(rootDir) {
336
- const files = [];
337
- const entries = await fs.readdir(rootDir, { withFileTypes: true });
338
- for (const entry of entries) {
339
- if (entry.isDirectory() && shouldIgnoreArtifactDirectory(entry.name)) {
340
- continue;
341
- }
342
- if (entry.isFile() && shouldIgnoreArtifactFile(entry.name)) {
343
- continue;
344
- }
345
- const fullPath = path.join(rootDir, entry.name);
346
- if (entry.isDirectory()) {
347
- files.push(...await collectArtifactFiles(fullPath));
348
- continue;
349
- }
350
- if (entry.isFile()) {
351
- files.push(fullPath);
352
- }
353
- }
354
- return files;
355
- }
356
- async function resolveArtifact(params, context) {
357
- const manifest = await readOrRefreshArtifactManifest(context);
358
- const artifact = findArtifact(manifest.items, params);
359
- if (!artifact) {
360
- throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, 'Artifact not found', {
361
- artifactId: params.artifactId,
362
- relativePath: params.relativePath
363
- });
364
- }
365
- const checkedPath = ensureInside(manifest.workspacePaths.workspaceRoot, artifact.localPath);
366
- if (!(await pathExists(checkedPath))) {
367
- throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, 'Artifact file does not exist', {
368
- localPath: checkedPath
369
- });
370
- }
371
- return {
372
- ...artifact,
373
- localPath: checkedPath
374
- };
375
- }
376
- function findArtifact(items, selector) {
377
- const artifactId = optionalTrimmedString(selector.artifactId);
378
- const relativePath = normalizeRelativePath(optionalTrimmedString(selector.relativePath));
379
- if (!artifactId && !relativePath) {
380
- throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, 'Either artifactId or relativePath is required');
381
- }
382
- return items.find((item) => {
383
- if (artifactId && item.id === artifactId) {
384
- return true;
385
- }
386
- return Boolean(relativePath && item.relativePath === relativePath);
387
- }) ?? null;
388
- }
389
- function resolveWorkspaceRoot(openclawRoot) {
390
- const workspaceRoot = path.join(openclawRoot, 'workspace');
391
- return ensureInside(openclawRoot, workspaceRoot);
392
- }
393
- async function ensureWorkspacePaths(openclawRoot) {
394
- const workspaceRoot = resolveWorkspaceRoot(openclawRoot);
395
- const manifestPath = ensureInside(workspaceRoot, path.join(workspaceRoot, ARTIFACT_MANIFEST_FILE));
396
- await ensureDir(workspaceRoot);
397
- return {
398
- workspaceRoot,
399
- manifestPath
400
- };
401
- }
402
- function buildArtifactId(relativePath) {
403
- return `art_${createHash('sha1').update(relativePath).digest('hex').slice(0, 12)}`;
404
- }
405
- function normalizeExtension(fileName) {
406
- const ext = path.extname(fileName).toLowerCase();
407
- return ext || null;
408
- }
409
- function normalizeRelativePath(value) {
410
- if (!value) {
411
- return null;
412
- }
413
- return value.replace(/\\/g, '/').replace(/^\/+/, '');
414
- }
415
- function normalizeTimestamp(created, fallback) {
416
- return Number.isNaN(created.getTime()) ? fallback.toISOString() : created.toISOString();
417
- }
418
- function normalizeMaxInlineBytes(value) {
419
- if (value === undefined) {
420
- return INLINE_CONTENT_LIMIT_BYTES;
421
- }
422
- if (!Number.isFinite(value) || value <= 0) {
423
- throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, 'maxInlineBytes must be a positive number');
424
- }
425
- return Math.floor(value);
426
- }
427
- function isPreviewableCategory(category) {
428
- return category === 'image' || category === 'video' || category === 'document';
429
- }
430
- function shouldIgnoreArtifactDirectory(dirName) {
431
- const normalized = dirName.trim().toLowerCase();
432
- return !normalized || normalized.startsWith('.') || IGNORE_DIRECTORY_NAMES.has(normalized);
433
- }
434
- function shouldIncludeArtifactFile(fileName) {
435
- if (shouldIgnoreArtifactFile(fileName)) {
436
- return false;
437
- }
438
- return classifyArtifactCategory(fileName) !== 'other';
439
- }
440
- function isArtifactRecord(value) {
441
- if (!value || Array.isArray(value) || typeof value !== 'object') {
442
- return false;
443
- }
444
- const objectValue = value;
445
- return (typeof objectValue.id === 'string' &&
446
- typeof objectValue.fileName === 'string' &&
447
- typeof objectValue.relativePath === 'string' &&
448
- typeof objectValue.localPath === 'string');
449
- }
450
- function artifactToJsonValue(item) {
451
- return {
452
- id: item.id,
453
- fileName: item.fileName,
454
- relativePath: item.relativePath,
455
- localPath: item.localPath,
456
- category: item.category,
457
- mimeType: item.mimeType,
458
- ext: item.ext,
459
- sizeBytes: item.sizeBytes,
460
- storageStatus: item.storageStatus,
461
- source: item.source,
462
- previewable: item.previewable,
463
- createdAt: item.createdAt,
464
- updatedAt: item.updatedAt,
465
- objectKey: item.objectKey ?? null,
466
- fileUrl: item.fileUrl ?? null,
467
- uploadedAt: item.uploadedAt ?? null,
468
- record: item.record ?? null
469
- };
470
- }
471
- function expectObject(value) {
472
- if (!value || Array.isArray(value) || typeof value !== 'object') {
473
- throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, 'Params must be an object');
474
- }
475
- return value;
476
- }
477
- function expectOptionalObject(value) {
478
- if (value === undefined) {
479
- return {};
480
- }
481
- return expectObject(value);
482
- }
483
- function buildPresignedPostBody(providedBody, artifact) {
484
- const body = isObject(providedBody) ? { ...providedBody } : {};
485
- if (body.filename === undefined) {
486
- body.filename = artifact.fileName;
487
- }
488
- return body;
489
- }
490
- function expectString(value, fieldName) {
491
- if (typeof value !== 'string' || value.trim().length === 0) {
492
- throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, `Field '${fieldName}' must be a non-empty string`);
493
- }
494
- return value.trim();
495
- }
496
- function optionalTrimmedString(value) {
497
- return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;
498
- }
499
- function isObject(value) {
500
- return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
501
- }
502
- async function resolveApiCoreBotEndpoint(openclawRoot, overrideBaseUrl, overrideAuthToken) {
503
- const config = await readJsonFile(path.join(openclawRoot, 'openclaw.json'));
504
- const pluginConfig = config.plugins?.entries?.['rol-websocket-channel']?.config?.apiCoreBot ?? {};
505
- const baseUrl = (overrideBaseUrl && overrideBaseUrl.trim()) || pluginConfig.baseUrl;
506
- if (!baseUrl || !baseUrl.trim()) {
507
- throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, 'apiCoreBot.baseUrl is not configured');
508
- }
509
- const authToken = (overrideAuthToken && overrideAuthToken.trim()) || pluginConfig.authToken;
510
- return {
511
- baseUrl: baseUrl.replace(/\/+$/, ''),
512
- authToken
513
- };
514
- }
515
- async function postJson(url, body, authToken) {
516
- const headers = {
517
- 'Content-Type': 'application/json'
518
- };
519
- if (authToken && authToken.trim()) {
520
- headers.Authorization = `Bearer ${authToken.trim()}`;
521
- }
522
- const response = await fetch(url, {
523
- method: 'POST',
524
- headers,
525
- body: JSON.stringify(body)
526
- });
527
- const payload = await response.json().catch(async () => await response.text());
528
- if (!response.ok) {
529
- throw new JsonRpcException(JSON_RPC_ERRORS.internalError, `Request failed: ${response.status}`, {
530
- url,
531
- status: response.status,
532
- payload
533
- });
534
- }
535
- if (isObject(payload) && payload.success === false) {
536
- throw new JsonRpcException(JSON_RPC_ERRORS.internalError, pickStringValue(payload, ['message', 'msg']) ?? 'Request failed', {
537
- url,
538
- payload
539
- });
540
- }
541
- return payload;
542
- }
543
- function resolvePresignedPostUploadTarget(payload) {
544
- for (const candidate of collectPresignedPostCandidates(payload)) {
545
- const uploadUrl = pickStringValue(candidate, ['url', 'uploadUrl', 'postUrl', 'presignedPostUrl']);
546
- const rawFields = candidate.fields ?? candidate.formData ?? candidate.form ?? candidate.params;
547
- const fields = toStringRecord(rawFields);
548
- if (!uploadUrl || !fields) {
549
- continue;
550
- }
551
- return {
552
- uploadUrl,
553
- fields,
554
- objectKey: pickStringValue(candidate, ['objectKey', 'fileKey', 'file_key', 'source_file_key', 'key']) ?? fields.key ?? null,
555
- fileUrl: pickStringValue(candidate, ['fileUrl', 'file_url', 'downloadUrl', 'source_file_url', 'location']) ?? null
556
- };
557
- }
558
- throw new JsonRpcException(JSON_RPC_ERRORS.internalError, 'Presigned post response is missing upload url or fields', { payload });
559
- }
560
- function collectPresignedPostCandidates(payload) {
561
- const candidates = [];
562
- const queue = [payload];
563
- while (queue.length > 0) {
564
- const current = queue.shift();
565
- if (!isObject(current)) {
566
- continue;
567
- }
568
- candidates.push(current);
569
- for (const key of ['data', 'result', 'presignedPost']) {
570
- const nested = current[key];
571
- if (isObject(nested)) {
572
- queue.push(nested);
573
- }
574
- }
575
- }
576
- return candidates;
577
- }
578
- function pickStringValue(source, keys) {
579
- for (const key of keys) {
580
- const value = source[key];
581
- if (typeof value === 'string' && value.trim().length > 0) {
582
- return value.trim();
583
- }
584
- }
585
- return null;
586
- }
587
- function toStringRecord(value) {
588
- if (!isObject(value)) {
589
- return null;
590
- }
591
- const result = {};
592
- for (const [key, item] of Object.entries(value)) {
593
- if (item === null) {
594
- continue;
595
- }
596
- if (typeof item === 'string') {
597
- result[key] = item;
598
- continue;
599
- }
600
- if (typeof item === 'number' || typeof item === 'boolean') {
601
- result[key] = String(item);
602
- continue;
603
- }
604
- return null;
605
- }
606
- return result;
607
- }
608
- async function uploadArtifactToPresignedPost(target, artifact) {
609
- const formData = new FormData();
610
- for (const [key, value] of Object.entries(target.fields)) {
611
- formData.append(key, value);
612
- }
613
- const content = await fs.readFile(artifact.localPath);
614
- formData.append('file', new Blob([content], { type: artifact.mimeType }), artifact.fileName);
615
- const response = await fetch(target.uploadUrl, {
616
- method: 'POST',
617
- body: formData
618
- });
619
- if (!response.ok) {
620
- const payload = await response.text().catch(() => '');
621
- throw new JsonRpcException(JSON_RPC_ERRORS.internalError, `Artifact upload failed: ${response.status}`, {
622
- url: target.uploadUrl,
623
- status: response.status,
624
- payload
625
- });
626
- }
627
- }
628
- function buildPublicFileUrl(uploadUrl, objectKey) {
629
- if (!objectKey) {
630
- return null;
631
- }
632
- try {
633
- const url = new URL(uploadUrl);
634
- url.pathname = `${url.pathname.replace(/\/+$/, '')}/${objectKey.replace(/^\/+/, '')}`;
635
- url.search = '';
636
- url.hash = '';
637
- return url.toString();
638
- }
639
- catch {
640
- return null;
641
- }
642
- }
1
+ import { createHash } from 'node:crypto';
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { ensureDir, pathExists, readJsonFile } from '../lib/fs.js';
5
+ import { ensureInside } from '../lib/paths.js';
6
+ import { JsonRpcException, JSON_RPC_ERRORS } from '../jsonrpc.js';
7
+ const INLINE_CONTENT_LIMIT_BYTES = 10 * 1024 * 1024;
8
+ const MIN_ARTIFACT_SIZE_BYTES = 1;
9
+ const ARTIFACT_MANIFEST_FILE = 'artifacts.json';
10
+ const MARKDOWN_SCAN_STATE_FILE = 'md-scan-state.json';
11
+ const PLUGIN_WORKSPACE_STATE_DIR = path.join('.openclaw', 'rol-websocket-channel');
12
+ const IGNORE_EXTENSIONS = new Set(['.tmp', '.part', '.crdownload']);
13
+ const IGNORE_FILE_NAMES = new Set(['.ds_store', 'thumbs.db', ARTIFACT_MANIFEST_FILE]);
14
+ const IGNORE_MARKDOWN_FILE_NAMES = new Set([
15
+ 'agents.md',
16
+ 'heartbeat.md',
17
+ 'identity.md',
18
+ 'memory.md',
19
+ 'soul.md',
20
+ 'tools.md',
21
+ 'user.md'
22
+ ]);
23
+ const IGNORE_DIRECTORY_NAMES = new Set([
24
+ '.git',
25
+ '.cache',
26
+ 'cache',
27
+ 'caches',
28
+ 'tmp',
29
+ 'temp',
30
+ 'logs',
31
+ 'log',
32
+ 'node_modules',
33
+ 'sessions',
34
+ 'session',
35
+ 'history'
36
+ ]);
37
+ const CATEGORY_BY_EXTENSION = {
38
+ '.png': 'image',
39
+ '.jpg': 'image',
40
+ '.jpeg': 'image',
41
+ '.webp': 'image',
42
+ '.gif': 'image',
43
+ '.svg': 'image',
44
+ '.mp4': 'video',
45
+ '.mov': 'video',
46
+ '.webm': 'video',
47
+ '.avi': 'video',
48
+ '.mkv': 'video',
49
+ '.m4v': 'video',
50
+ '.pdf': 'document',
51
+ '.doc': 'document',
52
+ '.docx': 'document',
53
+ '.md': 'document',
54
+ '.zip': 'archive',
55
+ '.7z': 'archive',
56
+ '.tar': 'archive',
57
+ '.gz': 'archive',
58
+ '.rar': 'archive'
59
+ };
60
+ const MIME_BY_EXTENSION = {
61
+ '.png': 'image/png',
62
+ '.jpg': 'image/jpeg',
63
+ '.jpeg': 'image/jpeg',
64
+ '.webp': 'image/webp',
65
+ '.gif': 'image/gif',
66
+ '.svg': 'image/svg+xml',
67
+ '.mp4': 'video/mp4',
68
+ '.mov': 'video/quicktime',
69
+ '.webm': 'video/webm',
70
+ '.avi': 'video/x-msvideo',
71
+ '.mkv': 'video/x-matroska',
72
+ '.m4v': 'video/x-m4v',
73
+ '.pdf': 'application/pdf',
74
+ '.doc': 'application/msword',
75
+ '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
76
+ '.md': 'text/markdown',
77
+ '.zip': 'application/zip',
78
+ '.7z': 'application/x-7z-compressed',
79
+ '.tar': 'application/x-tar',
80
+ '.gz': 'application/gzip',
81
+ '.rar': 'application/vnd.rar'
82
+ };
83
+ export const listArtifacts = async (params, context) => {
84
+ const objectParams = expectOptionalObject(params);
85
+ const refresh = objectParams.refresh !== false;
86
+ const manifest = refresh
87
+ ? await refreshArtifactManifest(context)
88
+ : await readOrRefreshArtifactManifest(context);
89
+ return {
90
+ scope: 'workspace',
91
+ count: manifest.items.length,
92
+ manifestPath: manifest.manifestPath,
93
+ workspaceRoot: manifest.workspacePaths.workspaceRoot,
94
+ items: manifest.items.map(artifactToJsonValue)
95
+ };
96
+ };
97
+ export const refreshArtifacts = async (params, context) => {
98
+ const objectParams = expectOptionalObject(params);
99
+ const manifest = await refreshArtifactManifest(context);
100
+ return {
101
+ ok: true,
102
+ scope: 'workspace',
103
+ count: manifest.items.length,
104
+ manifestPath: manifest.manifestPath,
105
+ workspaceRoot: manifest.workspacePaths.workspaceRoot,
106
+ items: manifest.items.map(artifactToJsonValue)
107
+ };
108
+ };
109
+ export const getArtifactContent = async (params, context) => {
110
+ const objectParams = expectObject(params);
111
+ const maxInlineBytes = normalizeMaxInlineBytes(objectParams.maxInlineBytes);
112
+ const artifact = await resolveArtifact(objectParams, context);
113
+ if (artifact.sizeBytes > maxInlineBytes) {
114
+ throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, 'Artifact is too large for inline transfer', {
115
+ artifactId: artifact.id,
116
+ sizeBytes: artifact.sizeBytes,
117
+ maxInlineBytes
118
+ });
119
+ }
120
+ const content = await fs.readFile(artifact.localPath);
121
+ return {
122
+ scope: 'workspace',
123
+ artifact: {
124
+ ...artifactToJsonValue(artifact),
125
+ contentBase64: content.toString('base64')
126
+ }
127
+ };
128
+ };
129
+ export const ensureArtifactUploaded = async (params, context) => {
130
+ const objectParams = expectObject(params);
131
+ const artifact = await resolveArtifact(objectParams, context);
132
+ if (artifact.storageStatus === 'uploaded' && artifact.fileUrl) {
133
+ return {
134
+ ok: true,
135
+ scope: 'workspace',
136
+ uploaded: false,
137
+ artifactId: artifact.id,
138
+ objectKey: artifact.objectKey ?? null,
139
+ downloadUrl: artifact.fileUrl,
140
+ item: artifactToJsonValue(artifact)
141
+ };
142
+ }
143
+ const endpointConfig = await resolveApiCoreBotEndpoint(context.openclawRoot, objectParams.baseUrl, objectParams.authToken);
144
+ const presignedPostBody = buildPresignedPostBody(objectParams.presignedPostBody, artifact);
145
+ const presignedResponse = await postJson(`${endpointConfig.baseUrl}/api-core-bot/front/s3/get-presigned-post`, presignedPostBody, endpointConfig.authToken);
146
+ const uploadTarget = resolvePresignedPostUploadTarget(presignedResponse);
147
+ await uploadArtifactToPresignedPost(uploadTarget, artifact);
148
+ const objectKey = uploadTarget.objectKey ?? artifact.objectKey ?? null;
149
+ const fileUrl = uploadTarget.fileUrl ?? artifact.fileUrl ?? buildPublicFileUrl(uploadTarget.uploadUrl, objectKey);
150
+ if (!fileUrl) {
151
+ throw new JsonRpcException(JSON_RPC_ERRORS.internalError, 'Unable to determine uploaded artifact download URL', {
152
+ artifactId: artifact.id,
153
+ objectKey
154
+ });
155
+ }
156
+ const manifest = await readOrRefreshArtifactManifest(context);
157
+ const updated = await persistUploadedArtifact(manifest.manifestPath, manifest.items, artifact.id, {
158
+ objectKey,
159
+ fileUrl,
160
+ record: artifact.record
161
+ });
162
+ return {
163
+ ok: true,
164
+ scope: 'workspace',
165
+ uploaded: true,
166
+ artifactId: artifact.id,
167
+ objectKey,
168
+ downloadUrl: fileUrl,
169
+ item: artifactToJsonValue(updated)
170
+ };
171
+ };
172
+ export const getArtifactPresignedPost = async (params, context) => {
173
+ const objectParams = expectObject(params);
174
+ const body = isObject(objectParams.body) ? objectParams.body : null;
175
+ if (!body) {
176
+ throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, 'Missing required parameter: body');
177
+ }
178
+ const endpointConfig = await resolveApiCoreBotEndpoint(context.openclawRoot, objectParams.baseUrl, objectParams.authToken);
179
+ const response = await postJson(`${endpointConfig.baseUrl}/api-core-bot/front/s3/get-presigned-post`, body, endpointConfig.authToken);
180
+ return {
181
+ ok: true,
182
+ endpoint: '/api-core-bot/front/s3/get-presigned-post',
183
+ data: response
184
+ };
185
+ };
186
+ export const createArtifactRecord = async (params, context) => {
187
+ const objectParams = expectObject(params);
188
+ const body = isObject(objectParams.body) ? objectParams.body : null;
189
+ if (!body) {
190
+ throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, 'Missing required parameter: body');
191
+ }
192
+ const endpoint = expectString(objectParams.endpoint, 'endpoint');
193
+ if (!endpoint.startsWith('/api-core-bot/front/')) {
194
+ throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, 'endpoint must start with /api-core-bot/front/');
195
+ }
196
+ const endpointConfig = await resolveApiCoreBotEndpoint(context.openclawRoot, objectParams.baseUrl, objectParams.authToken);
197
+ const response = await postJson(`${endpointConfig.baseUrl}${endpoint}`, body, endpointConfig.authToken);
198
+ return {
199
+ ok: true,
200
+ endpoint,
201
+ data: response
202
+ };
203
+ };
204
+ export const markArtifactUploaded = async (params, context) => {
205
+ const objectParams = expectObject(params);
206
+ const objectKey = optionalTrimmedString(objectParams.objectKey);
207
+ const fileUrl = optionalTrimmedString(objectParams.fileUrl);
208
+ if (!objectKey && !fileUrl) {
209
+ throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, 'At least one of objectKey or fileUrl is required');
210
+ }
211
+ const manifest = await readOrRefreshArtifactManifest(context);
212
+ const artifact = findArtifact(manifest.items, objectParams);
213
+ if (!artifact) {
214
+ throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, 'Artifact not found', {
215
+ artifactId: objectParams.artifactId,
216
+ relativePath: objectParams.relativePath
217
+ });
218
+ }
219
+ const updated = await persistUploadedArtifact(manifest.manifestPath, manifest.items, artifact.id, {
220
+ objectKey: objectKey ?? artifact.objectKey ?? null,
221
+ fileUrl: fileUrl ?? artifact.fileUrl ?? null,
222
+ record: objectParams.record === undefined ? artifact.record : objectParams.record
223
+ });
224
+ return {
225
+ ok: true,
226
+ scope: 'workspace',
227
+ artifactId: artifact.id,
228
+ item: artifactToJsonValue(updated)
229
+ };
230
+ };
231
+ export function classifyArtifactCategory(fileName) {
232
+ const ext = path.extname(fileName).toLowerCase();
233
+ return CATEGORY_BY_EXTENSION[ext] ?? 'other';
234
+ }
235
+ export function shouldIgnoreArtifactFile(fileName) {
236
+ const normalized = fileName.trim().toLowerCase();
237
+ if (!normalized) {
238
+ return true;
239
+ }
240
+ if (normalized.startsWith('.')) {
241
+ return true;
242
+ }
243
+ if (IGNORE_FILE_NAMES.has(normalized)) {
244
+ return true;
245
+ }
246
+ const ext = path.extname(normalized);
247
+ return IGNORE_EXTENSIONS.has(ext);
248
+ }
249
+ async function refreshArtifactManifest(context) {
250
+ const workspacePaths = await ensureWorkspacePaths(context.openclawRoot);
251
+ const { workspaceRoot, manifestPath } = workspacePaths;
252
+ const existing = await readExistingManifest(manifestPath);
253
+ const existingByPath = new Map(existing.map((item) => [item.relativePath, item]));
254
+ const loadedMarkdownScanState = await readMarkdownScanState(workspacePaths.markdownScanStatePath);
255
+ const hadMarkdownScanState = loadedMarkdownScanState !== null;
256
+ const markdownScanState = loadedMarkdownScanState ?? createMarkdownScanState();
257
+ const files = await collectArtifactFiles(workspaceRoot);
258
+ const items = [];
259
+ for (const fullPath of files) {
260
+ const stat = await fs.stat(fullPath);
261
+ if (!stat.isFile() || stat.size < MIN_ARTIFACT_SIZE_BYTES) {
262
+ continue;
263
+ }
264
+ const relativePath = path.relative(workspaceRoot, fullPath).replace(/\\/g, '/');
265
+ const existingItem = existingByPath.get(relativePath);
266
+ const fileName = path.basename(fullPath);
267
+ const isMarkdown = isMarkdownArtifactFile(fileName);
268
+ if (isMarkdown) {
269
+ if (!shouldIncludeMarkdownArtifactFile(relativePath, fileName, stat, markdownScanState, hadMarkdownScanState)) {
270
+ continue;
271
+ }
272
+ }
273
+ else if (!shouldIncludeArtifactFile(fileName)) {
274
+ continue;
275
+ }
276
+ const ext = normalizeExtension(fileName);
277
+ const createdAt = normalizeTimestamp(stat.birthtime, stat.mtime);
278
+ const updatedAt = stat.mtime.toISOString();
279
+ items.push({
280
+ id: existingItem?.id ?? buildArtifactId(relativePath),
281
+ fileName,
282
+ relativePath,
283
+ localPath: fullPath,
284
+ category: classifyArtifactCategory(fileName),
285
+ mimeType: MIME_BY_EXTENSION[ext ?? ''] ?? 'application/octet-stream',
286
+ ext,
287
+ sizeBytes: stat.size,
288
+ storageStatus: existingItem?.storageStatus ?? 'local_only',
289
+ source: existingItem?.source ?? 'generated',
290
+ previewable: isPreviewableCategory(classifyArtifactCategory(fileName)),
291
+ createdAt: existingItem?.createdAt ?? createdAt,
292
+ updatedAt,
293
+ objectKey: existingItem?.objectKey ?? null,
294
+ fileUrl: existingItem?.fileUrl ?? null,
295
+ uploadedAt: existingItem?.uploadedAt ?? null,
296
+ record: existingItem?.record
297
+ });
298
+ }
299
+ items.sort((a, b) => a.fileName.localeCompare(b.fileName));
300
+ markdownScanState.lastScanAt = new Date().toISOString();
301
+ await writeMarkdownScanState(workspacePaths.markdownScanStatePath, markdownScanState);
302
+ await writeArtifactManifest(manifestPath, items);
303
+ return { manifestPath, items, workspacePaths };
304
+ }
305
+ async function readOrRefreshArtifactManifest(context) {
306
+ const workspacePaths = await ensureWorkspacePaths(context.openclawRoot);
307
+ const { manifestPath } = workspacePaths;
308
+ if (!(await pathExists(manifestPath))) {
309
+ return await refreshArtifactManifest(context);
310
+ }
311
+ const items = await readExistingManifest(manifestPath);
312
+ return { manifestPath, items, workspacePaths };
313
+ }
314
+ async function readExistingManifest(manifestPath) {
315
+ if (!(await pathExists(manifestPath))) {
316
+ return [];
317
+ }
318
+ const parsed = await readJsonFile(manifestPath);
319
+ if (!Array.isArray(parsed)) {
320
+ return [];
321
+ }
322
+ const items = [];
323
+ for (const item of parsed) {
324
+ if (isArtifactRecord(item)) {
325
+ items.push(item);
326
+ }
327
+ }
328
+ return items;
329
+ }
330
+ async function writeArtifactManifest(manifestPath, items) {
331
+ await fs.writeFile(manifestPath, JSON.stringify(items.map((item) => artifactToJsonValue(item)), null, 2), 'utf8');
332
+ }
333
+ async function readMarkdownScanState(statePath) {
334
+ if (!(await pathExists(statePath))) {
335
+ return null;
336
+ }
337
+ const parsed = await readJsonFile(statePath);
338
+ if (!isObject(parsed)) {
339
+ return null;
340
+ }
341
+ const files = {};
342
+ if (isObject(parsed.files)) {
343
+ for (const [relativePath, value] of Object.entries(parsed.files)) {
344
+ if (isMarkdownScanStateFile(value)) {
345
+ files[relativePath] = value;
346
+ }
347
+ }
348
+ }
349
+ return {
350
+ initializedAt: typeof parsed.initializedAt === 'string' ? parsed.initializedAt : new Date().toISOString(),
351
+ lastScanAt: typeof parsed.lastScanAt === 'string' ? parsed.lastScanAt : new Date().toISOString(),
352
+ files
353
+ };
354
+ }
355
+ async function writeMarkdownScanState(statePath, state) {
356
+ await ensureDir(path.dirname(statePath));
357
+ await fs.writeFile(statePath, JSON.stringify(state, null, 2), 'utf8');
358
+ }
359
+ async function persistUploadedArtifact(manifestPath, items, artifactId, updates) {
360
+ const now = new Date().toISOString();
361
+ let updatedArtifact = null;
362
+ const updatedItems = items.map((item) => {
363
+ if (!isArtifactRecord(item) || item.id !== artifactId) {
364
+ return item;
365
+ }
366
+ updatedArtifact = {
367
+ ...item,
368
+ storageStatus: 'uploaded',
369
+ objectKey: updates.objectKey ?? item.objectKey ?? null,
370
+ fileUrl: updates.fileUrl ?? item.fileUrl ?? null,
371
+ uploadedAt: now,
372
+ updatedAt: now,
373
+ ...(updates.record === undefined ? {} : { record: updates.record })
374
+ };
375
+ return updatedArtifact;
376
+ });
377
+ await writeArtifactManifest(manifestPath, updatedItems);
378
+ if (!updatedArtifact) {
379
+ throw new JsonRpcException(JSON_RPC_ERRORS.internalError, 'Artifact not found while persisting upload state', {
380
+ artifactId
381
+ });
382
+ }
383
+ return updatedArtifact;
384
+ }
385
+ async function collectArtifactFiles(rootDir) {
386
+ const files = [];
387
+ const entries = await fs.readdir(rootDir, { withFileTypes: true });
388
+ for (const entry of entries) {
389
+ if (entry.isDirectory() && shouldIgnoreArtifactDirectory(entry.name)) {
390
+ continue;
391
+ }
392
+ if (entry.isFile() && shouldIgnoreArtifactFile(entry.name)) {
393
+ continue;
394
+ }
395
+ const fullPath = path.join(rootDir, entry.name);
396
+ if (entry.isDirectory()) {
397
+ files.push(...await collectArtifactFiles(fullPath));
398
+ continue;
399
+ }
400
+ if (entry.isFile()) {
401
+ files.push(fullPath);
402
+ }
403
+ }
404
+ return files;
405
+ }
406
+ async function resolveArtifact(params, context) {
407
+ const manifest = await readOrRefreshArtifactManifest(context);
408
+ const artifact = findArtifact(manifest.items, params);
409
+ if (!artifact) {
410
+ throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, 'Artifact not found', {
411
+ artifactId: params.artifactId,
412
+ relativePath: params.relativePath
413
+ });
414
+ }
415
+ const checkedPath = ensureInside(manifest.workspacePaths.workspaceRoot, artifact.localPath);
416
+ if (!(await pathExists(checkedPath))) {
417
+ throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, 'Artifact file does not exist', {
418
+ localPath: checkedPath
419
+ });
420
+ }
421
+ return {
422
+ ...artifact,
423
+ localPath: checkedPath
424
+ };
425
+ }
426
+ function findArtifact(items, selector) {
427
+ const artifactId = optionalTrimmedString(selector.artifactId);
428
+ const relativePath = normalizeRelativePath(optionalTrimmedString(selector.relativePath));
429
+ if (!artifactId && !relativePath) {
430
+ throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, 'Either artifactId or relativePath is required');
431
+ }
432
+ return items.find((item) => {
433
+ if (artifactId && item.id === artifactId) {
434
+ return true;
435
+ }
436
+ return Boolean(relativePath && item.relativePath === relativePath);
437
+ }) ?? null;
438
+ }
439
+ function resolveWorkspaceRoot(openclawRoot) {
440
+ const workspaceRoot = path.join(openclawRoot, 'workspace');
441
+ return ensureInside(openclawRoot, workspaceRoot);
442
+ }
443
+ async function ensureWorkspacePaths(openclawRoot) {
444
+ const workspaceRoot = resolveWorkspaceRoot(openclawRoot);
445
+ const manifestPath = ensureInside(workspaceRoot, path.join(workspaceRoot, ARTIFACT_MANIFEST_FILE));
446
+ const markdownScanStatePath = ensureInside(workspaceRoot, path.join(workspaceRoot, PLUGIN_WORKSPACE_STATE_DIR, MARKDOWN_SCAN_STATE_FILE));
447
+ await ensureDir(workspaceRoot);
448
+ return {
449
+ workspaceRoot,
450
+ manifestPath,
451
+ markdownScanStatePath
452
+ };
453
+ }
454
+ function createMarkdownScanState() {
455
+ const now = new Date().toISOString();
456
+ return {
457
+ initializedAt: now,
458
+ lastScanAt: now,
459
+ files: {}
460
+ };
461
+ }
462
+ function buildArtifactId(relativePath) {
463
+ return `art_${createHash('sha1').update(relativePath).digest('hex').slice(0, 12)}`;
464
+ }
465
+ function normalizeExtension(fileName) {
466
+ const ext = path.extname(fileName).toLowerCase();
467
+ return ext || null;
468
+ }
469
+ function isMarkdownArtifactFile(fileName) {
470
+ return path.extname(fileName).toLowerCase() === '.md';
471
+ }
472
+ function normalizeRelativePath(value) {
473
+ if (!value) {
474
+ return null;
475
+ }
476
+ return value.replace(/\\/g, '/').replace(/^\/+/, '');
477
+ }
478
+ function normalizeTimestamp(created, fallback) {
479
+ return Number.isNaN(created.getTime()) ? fallback.toISOString() : created.toISOString();
480
+ }
481
+ function normalizeMaxInlineBytes(value) {
482
+ if (value === undefined) {
483
+ return INLINE_CONTENT_LIMIT_BYTES;
484
+ }
485
+ if (!Number.isFinite(value) || value <= 0) {
486
+ throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, 'maxInlineBytes must be a positive number');
487
+ }
488
+ return Math.floor(value);
489
+ }
490
+ function isPreviewableCategory(category) {
491
+ return category === 'image' || category === 'video' || category === 'document';
492
+ }
493
+ function shouldIgnoreArtifactDirectory(dirName) {
494
+ const normalized = dirName.trim().toLowerCase();
495
+ return !normalized || normalized.startsWith('.') || IGNORE_DIRECTORY_NAMES.has(normalized);
496
+ }
497
+ function shouldIncludeArtifactFile(fileName) {
498
+ if (shouldIgnoreArtifactFile(fileName)) {
499
+ return false;
500
+ }
501
+ return classifyArtifactCategory(fileName) !== 'other';
502
+ }
503
+ function shouldIncludeMarkdownArtifactFile(relativePath, fileName, stat, state, hadState) {
504
+ if (shouldIgnoreArtifactFile(fileName)) {
505
+ return false;
506
+ }
507
+ if (IGNORE_MARKDOWN_FILE_NAMES.has(fileName.trim().toLowerCase())) {
508
+ return false;
509
+ }
510
+ const existing = state.files[relativePath];
511
+ if (existing) {
512
+ existing.sizeBytes = stat.size;
513
+ existing.mtimeMs = stat.mtimeMs;
514
+ return !existing.baseline;
515
+ }
516
+ state.files[relativePath] = {
517
+ sizeBytes: stat.size,
518
+ mtimeMs: stat.mtimeMs,
519
+ firstSeenAt: new Date().toISOString(),
520
+ baseline: !hadState
521
+ };
522
+ return hadState;
523
+ }
524
+ function isMarkdownScanStateFile(value) {
525
+ if (!value || Array.isArray(value) || typeof value !== 'object') {
526
+ return false;
527
+ }
528
+ const objectValue = value;
529
+ return (typeof objectValue.sizeBytes === 'number' &&
530
+ typeof objectValue.mtimeMs === 'number' &&
531
+ typeof objectValue.firstSeenAt === 'string' &&
532
+ typeof objectValue.baseline === 'boolean');
533
+ }
534
+ function isArtifactRecord(value) {
535
+ if (!value || Array.isArray(value) || typeof value !== 'object') {
536
+ return false;
537
+ }
538
+ const objectValue = value;
539
+ return (typeof objectValue.id === 'string' &&
540
+ typeof objectValue.fileName === 'string' &&
541
+ typeof objectValue.relativePath === 'string' &&
542
+ typeof objectValue.localPath === 'string');
543
+ }
544
+ function artifactToJsonValue(item) {
545
+ return {
546
+ id: item.id,
547
+ fileName: item.fileName,
548
+ relativePath: item.relativePath,
549
+ localPath: item.localPath,
550
+ category: item.category,
551
+ mimeType: item.mimeType,
552
+ ext: item.ext,
553
+ sizeBytes: item.sizeBytes,
554
+ storageStatus: item.storageStatus,
555
+ source: item.source,
556
+ previewable: item.previewable,
557
+ createdAt: item.createdAt,
558
+ updatedAt: item.updatedAt,
559
+ objectKey: item.objectKey ?? null,
560
+ fileUrl: item.fileUrl ?? null,
561
+ uploadedAt: item.uploadedAt ?? null,
562
+ record: item.record ?? null
563
+ };
564
+ }
565
+ function expectObject(value) {
566
+ if (!value || Array.isArray(value) || typeof value !== 'object') {
567
+ throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, 'Params must be an object');
568
+ }
569
+ return value;
570
+ }
571
+ function expectOptionalObject(value) {
572
+ if (value === undefined) {
573
+ return {};
574
+ }
575
+ return expectObject(value);
576
+ }
577
+ function buildPresignedPostBody(providedBody, artifact) {
578
+ const body = isObject(providedBody) ? { ...providedBody } : {};
579
+ if (body.filename === undefined) {
580
+ body.filename = artifact.fileName;
581
+ }
582
+ return body;
583
+ }
584
+ function expectString(value, fieldName) {
585
+ if (typeof value !== 'string' || value.trim().length === 0) {
586
+ throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, `Field '${fieldName}' must be a non-empty string`);
587
+ }
588
+ return value.trim();
589
+ }
590
+ function optionalTrimmedString(value) {
591
+ return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;
592
+ }
593
+ function isObject(value) {
594
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
595
+ }
596
+ async function resolveApiCoreBotEndpoint(openclawRoot, overrideBaseUrl, overrideAuthToken) {
597
+ const config = await readJsonFile(path.join(openclawRoot, 'openclaw.json'));
598
+ const pluginConfig = config.plugins?.entries?.['rol-websocket-channel']?.config?.apiCoreBot ?? {};
599
+ const baseUrl = (overrideBaseUrl && overrideBaseUrl.trim()) || pluginConfig.baseUrl;
600
+ if (!baseUrl || !baseUrl.trim()) {
601
+ throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, 'apiCoreBot.baseUrl is not configured');
602
+ }
603
+ const authToken = (overrideAuthToken && overrideAuthToken.trim()) || pluginConfig.authToken;
604
+ return {
605
+ baseUrl: baseUrl.replace(/\/+$/, ''),
606
+ authToken
607
+ };
608
+ }
609
+ async function postJson(url, body, authToken) {
610
+ const headers = {
611
+ 'Content-Type': 'application/json'
612
+ };
613
+ if (authToken && authToken.trim()) {
614
+ headers.Authorization = `Bearer ${authToken.trim()}`;
615
+ }
616
+ const response = await fetch(url, {
617
+ method: 'POST',
618
+ headers,
619
+ body: JSON.stringify(body)
620
+ });
621
+ const payload = await response.json().catch(async () => await response.text());
622
+ if (!response.ok) {
623
+ throw new JsonRpcException(JSON_RPC_ERRORS.internalError, `Request failed: ${response.status}`, {
624
+ url,
625
+ status: response.status,
626
+ payload
627
+ });
628
+ }
629
+ if (isObject(payload) && payload.success === false) {
630
+ throw new JsonRpcException(JSON_RPC_ERRORS.internalError, pickStringValue(payload, ['message', 'msg']) ?? 'Request failed', {
631
+ url,
632
+ payload
633
+ });
634
+ }
635
+ return payload;
636
+ }
637
+ function resolvePresignedPostUploadTarget(payload) {
638
+ for (const candidate of collectPresignedPostCandidates(payload)) {
639
+ const uploadUrl = pickStringValue(candidate, ['url', 'uploadUrl', 'postUrl', 'presignedPostUrl']);
640
+ const rawFields = candidate.fields ?? candidate.formData ?? candidate.form ?? candidate.params;
641
+ const fields = toStringRecord(rawFields);
642
+ if (!uploadUrl || !fields) {
643
+ continue;
644
+ }
645
+ return {
646
+ uploadUrl,
647
+ fields,
648
+ objectKey: pickStringValue(candidate, ['objectKey', 'fileKey', 'file_key', 'source_file_key', 'key']) ?? fields.key ?? null,
649
+ fileUrl: pickStringValue(candidate, ['fileUrl', 'file_url', 'downloadUrl', 'source_file_url', 'location']) ?? null
650
+ };
651
+ }
652
+ throw new JsonRpcException(JSON_RPC_ERRORS.internalError, 'Presigned post response is missing upload url or fields', { payload });
653
+ }
654
+ function collectPresignedPostCandidates(payload) {
655
+ const candidates = [];
656
+ const queue = [payload];
657
+ while (queue.length > 0) {
658
+ const current = queue.shift();
659
+ if (!isObject(current)) {
660
+ continue;
661
+ }
662
+ candidates.push(current);
663
+ for (const key of ['data', 'result', 'presignedPost']) {
664
+ const nested = current[key];
665
+ if (isObject(nested)) {
666
+ queue.push(nested);
667
+ }
668
+ }
669
+ }
670
+ return candidates;
671
+ }
672
+ function pickStringValue(source, keys) {
673
+ for (const key of keys) {
674
+ const value = source[key];
675
+ if (typeof value === 'string' && value.trim().length > 0) {
676
+ return value.trim();
677
+ }
678
+ }
679
+ return null;
680
+ }
681
+ function toStringRecord(value) {
682
+ if (!isObject(value)) {
683
+ return null;
684
+ }
685
+ const result = {};
686
+ for (const [key, item] of Object.entries(value)) {
687
+ if (item === null) {
688
+ continue;
689
+ }
690
+ if (typeof item === 'string') {
691
+ result[key] = item;
692
+ continue;
693
+ }
694
+ if (typeof item === 'number' || typeof item === 'boolean') {
695
+ result[key] = String(item);
696
+ continue;
697
+ }
698
+ return null;
699
+ }
700
+ return result;
701
+ }
702
+ async function uploadArtifactToPresignedPost(target, artifact) {
703
+ const formData = new FormData();
704
+ for (const [key, value] of Object.entries(target.fields)) {
705
+ formData.append(key, value);
706
+ }
707
+ const content = await fs.readFile(artifact.localPath);
708
+ formData.append('file', new Blob([content], { type: artifact.mimeType }), artifact.fileName);
709
+ const response = await fetch(target.uploadUrl, {
710
+ method: 'POST',
711
+ body: formData
712
+ });
713
+ if (!response.ok) {
714
+ const payload = await response.text().catch(() => '');
715
+ throw new JsonRpcException(JSON_RPC_ERRORS.internalError, `Artifact upload failed: ${response.status}`, {
716
+ url: target.uploadUrl,
717
+ status: response.status,
718
+ payload
719
+ });
720
+ }
721
+ }
722
+ function buildPublicFileUrl(uploadUrl, objectKey) {
723
+ if (!objectKey) {
724
+ return null;
725
+ }
726
+ try {
727
+ const url = new URL(uploadUrl);
728
+ url.pathname = `${url.pathname.replace(/\/+$/, '')}/${objectKey.replace(/^\/+/, '')}`;
729
+ url.search = '';
730
+ url.hash = '';
731
+ return url.toString();
732
+ }
733
+ catch {
734
+ return null;
735
+ }
736
+ }