rol-websocket-channel 1.7.4 → 1.7.6
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 +45 -6
- package/dist/src/admin/lib/paths.js +10 -10
- package/dist/src/admin/methods/pairing.js +12 -57
- package/dist/src/admin/methods/sessions-extended.js +19 -13
- package/index.ts +54 -7
- package/package.json +1 -1
- package/src/admin/lib/paths.ts +10 -10
- package/src/admin/methods/pairing.ts +12 -61
- package/src/admin/methods/sessions-extended.ts +25 -16
- package/tsconfig.json +1 -1
- package/dist/src/admin/cli-manifest.test.js +0 -122
- package/dist/src/admin/lib/openclaw-bin.test.js +0 -37
- package/dist/src/admin/methods/admin.test.js +0 -61
- package/dist/src/admin/methods/artifacts.test.js +0 -304
- package/dist/src/admin/methods/index.test.js +0 -27
- package/dist/src/admin/methods/mem9.test.js +0 -259
- package/dist/src/admin/methods/pairing.test.js +0 -114
- package/dist/src/admin/tools/artifacts-tools.test.js +0 -163
- package/dist/src/mqtt/mqtt.test.js +0 -418
- package/src/admin/cli-manifest.test.ts +0 -135
- package/src/admin/lib/openclaw-bin.test.ts +0 -38
- package/src/admin/methods/admin.test.ts +0 -91
- package/src/admin/methods/artifacts.test.ts +0 -486
- package/src/admin/methods/index.test.ts +0 -34
- package/src/admin/methods/mem9.test.ts +0 -318
- package/src/admin/methods/pairing.test.ts +0 -149
- package/src/admin/tools/artifacts-tools.test.ts +0 -215
- package/src/mqtt/mqtt.test.ts +0 -670
- package/test/admin/methods/models-extended.test.ts +0 -49
- package/test/message-handler-artifacts.test.ts +0 -11
|
@@ -26,9 +26,10 @@ export const getSession: MethodHandler = async (
|
|
|
26
26
|
: undefined;
|
|
27
27
|
const sessionId = typeof objectParams.sessionId === 'string' ? objectParams.sessionId : null;
|
|
28
28
|
const requestedLimit = typeof objectParams.limit === 'number' ? objectParams.limit : MAX_SESSION_MESSAGES;
|
|
29
|
-
const
|
|
29
|
+
const beforeId = typeof objectParams.beforeId === 'string' && objectParams.beforeId.trim()
|
|
30
|
+
? objectParams.beforeId.trim()
|
|
31
|
+
: undefined;
|
|
30
32
|
const limit = normalizePageSize(requestedLimit, MAX_SESSION_MESSAGES);
|
|
31
|
-
const offset = normalizeOffset(requestedOffset);
|
|
32
33
|
|
|
33
34
|
if (!sessionId) {
|
|
34
35
|
throw new JsonRpcException(
|
|
@@ -52,7 +53,7 @@ export const getSession: MethodHandler = async (
|
|
|
52
53
|
'sessions',
|
|
53
54
|
`${session.sessionId ?? sessionId}.jsonl`
|
|
54
55
|
);
|
|
55
|
-
const messages = await readSessionMessages(sessionFile, limit,
|
|
56
|
+
const messages = await readSessionMessages(sessionFile, limit, beforeId);
|
|
56
57
|
|
|
57
58
|
return {
|
|
58
59
|
agentId: session.agentId,
|
|
@@ -69,7 +70,8 @@ export const getSession: MethodHandler = async (
|
|
|
69
70
|
messages: {
|
|
70
71
|
total: messages.total,
|
|
71
72
|
limit,
|
|
72
|
-
|
|
73
|
+
hasMore: messages.hasMore,
|
|
74
|
+
nextBeforeId: messages.nextBeforeId,
|
|
73
75
|
items: messages.items
|
|
74
76
|
}
|
|
75
77
|
};
|
|
@@ -203,21 +205,13 @@ function normalizePageSize(value: number, max: number): number {
|
|
|
203
205
|
return Math.min(normalized, max);
|
|
204
206
|
}
|
|
205
207
|
|
|
206
|
-
function normalizeOffset(value: number): number {
|
|
207
|
-
if (!Number.isFinite(value)) {
|
|
208
|
-
return 0;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
return Math.max(0, Math.floor(value));
|
|
212
|
-
}
|
|
213
|
-
|
|
214
208
|
async function readSessionMessages(
|
|
215
209
|
filePath: string,
|
|
216
210
|
limit: number,
|
|
217
|
-
|
|
218
|
-
): Promise<{ total: number; items: SessionMessage[] }> {
|
|
211
|
+
beforeId?: string
|
|
212
|
+
): Promise<{ total: number; hasMore: boolean; nextBeforeId: string | null; items: SessionMessage[] }> {
|
|
219
213
|
if (!(await pathExists(filePath))) {
|
|
220
|
-
return { total: 0, items: [] };
|
|
214
|
+
return { total: 0, hasMore: false, nextBeforeId: null, items: [] };
|
|
221
215
|
}
|
|
222
216
|
|
|
223
217
|
const messages: SessionMessage[] = [];
|
|
@@ -239,9 +233,24 @@ async function readSessionMessages(
|
|
|
239
233
|
}
|
|
240
234
|
}
|
|
241
235
|
|
|
236
|
+
const endExclusive = beforeId ? messages.findIndex((message) => message.id === beforeId) : messages.length;
|
|
237
|
+
if (endExclusive < 0) {
|
|
238
|
+
throw new JsonRpcException(
|
|
239
|
+
JSON_RPC_ERRORS.invalidParams,
|
|
240
|
+
`beforeId not found: ${beforeId}`
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const start = Math.max(0, endExclusive - limit);
|
|
245
|
+
const items = messages.slice(start, endExclusive);
|
|
246
|
+
const hasMore = start > 0;
|
|
247
|
+
const firstItemId = typeof items[0]?.id === 'string' ? items[0].id : null;
|
|
248
|
+
|
|
242
249
|
return {
|
|
243
250
|
total: messages.length,
|
|
244
|
-
|
|
251
|
+
hasMore,
|
|
252
|
+
nextBeforeId: hasMore ? firstItemId : null,
|
|
253
|
+
items
|
|
245
254
|
};
|
|
246
255
|
}
|
|
247
256
|
|
package/tsconfig.json
CHANGED
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import { readFileSync } from 'node:fs';
|
|
3
|
-
import test from 'node:test';
|
|
4
|
-
import entry, { formatCliErrorPayload } from '../../index.js';
|
|
5
|
-
const manifest = JSON.parse(readFileSync(new URL('../../openclaw.plugin.json', import.meta.url), 'utf8'));
|
|
6
|
-
const packageJson = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
|
|
7
|
-
test('manifest declares OpenClaw 2026 command ownership for admin bridge CLI', () => {
|
|
8
|
-
assert.deepEqual(manifest.activation?.onCommands, ['admin-bridge', 'rol-websocket-channel']);
|
|
9
|
-
assert.deepEqual(manifest.commandAliases, [
|
|
10
|
-
{ name: 'admin-bridge' },
|
|
11
|
-
{ name: 'rol-websocket-channel' }
|
|
12
|
-
]);
|
|
13
|
-
assert.deepEqual(manifest.contracts?.tools, [
|
|
14
|
-
'rol_artifacts_find_latest',
|
|
15
|
-
'rol_artifacts_publish'
|
|
16
|
-
]);
|
|
17
|
-
});
|
|
18
|
-
test('package.json declares official OpenClaw runtime metadata', () => {
|
|
19
|
-
assert.deepEqual(packageJson.openclaw?.extensions, ['./index.ts']);
|
|
20
|
-
assert.deepEqual(packageJson.openclaw?.runtimeExtensions, ['./dist/index.js']);
|
|
21
|
-
assert.deepEqual(packageJson.openclaw?.compat, {
|
|
22
|
-
pluginApi: '>=2026.5.7',
|
|
23
|
-
minGatewayVersion: '2026.5.7'
|
|
24
|
-
});
|
|
25
|
-
assert.deepEqual(packageJson.openclaw?.build, {
|
|
26
|
-
openclawVersion: '2026.5.7',
|
|
27
|
-
pluginSdkVersion: '2026.5.7'
|
|
28
|
-
});
|
|
29
|
-
});
|
|
30
|
-
test('runtime entry exposes admin-bridge CLI roots and registers artifacts tools in full mode', () => {
|
|
31
|
-
const descriptors = [];
|
|
32
|
-
const commands = [];
|
|
33
|
-
const aliases = [];
|
|
34
|
-
const registeredTools = [];
|
|
35
|
-
const commandNode = {
|
|
36
|
-
alias(name) {
|
|
37
|
-
aliases.push(name);
|
|
38
|
-
return commandNode;
|
|
39
|
-
},
|
|
40
|
-
description() {
|
|
41
|
-
return commandNode;
|
|
42
|
-
},
|
|
43
|
-
addHelpText() {
|
|
44
|
-
return commandNode;
|
|
45
|
-
},
|
|
46
|
-
option() {
|
|
47
|
-
return commandNode;
|
|
48
|
-
},
|
|
49
|
-
action() {
|
|
50
|
-
return commandNode;
|
|
51
|
-
},
|
|
52
|
-
command(name) {
|
|
53
|
-
commands.push(name);
|
|
54
|
-
return commandNode;
|
|
55
|
-
}
|
|
56
|
-
};
|
|
57
|
-
const api = {
|
|
58
|
-
registrationMode: 'full',
|
|
59
|
-
runtime: {},
|
|
60
|
-
registerChannel() { },
|
|
61
|
-
registerTool(tool) {
|
|
62
|
-
registeredTools.push(tool);
|
|
63
|
-
},
|
|
64
|
-
registerCli(callback, options) {
|
|
65
|
-
descriptors.push(...options.descriptors);
|
|
66
|
-
callback({ program: commandNode });
|
|
67
|
-
}
|
|
68
|
-
};
|
|
69
|
-
entry.register(api);
|
|
70
|
-
assert.deepEqual(descriptors, [
|
|
71
|
-
{
|
|
72
|
-
name: 'admin-bridge',
|
|
73
|
-
description: 'OpenClaw admin bridge commands',
|
|
74
|
-
hasSubcommands: true
|
|
75
|
-
},
|
|
76
|
-
{
|
|
77
|
-
name: 'rol-websocket-channel',
|
|
78
|
-
description: 'OpenClaw admin bridge commands',
|
|
79
|
-
hasSubcommands: true
|
|
80
|
-
}
|
|
81
|
-
]);
|
|
82
|
-
assert.equal(commands[0], 'admin-bridge');
|
|
83
|
-
assert.equal(aliases[0], 'rol-websocket-channel');
|
|
84
|
-
assert.equal(registeredTools.length, 1);
|
|
85
|
-
});
|
|
86
|
-
test('CLI metadata mode skips full-only tool registration', () => {
|
|
87
|
-
let registerToolCalled = false;
|
|
88
|
-
const api = {
|
|
89
|
-
registrationMode: 'cli-metadata',
|
|
90
|
-
runtime: {},
|
|
91
|
-
registerChannel() { },
|
|
92
|
-
registerTool() {
|
|
93
|
-
registerToolCalled = true;
|
|
94
|
-
},
|
|
95
|
-
registerCli() { }
|
|
96
|
-
};
|
|
97
|
-
entry.register(api);
|
|
98
|
-
assert.equal(registerToolCalled, false);
|
|
99
|
-
});
|
|
100
|
-
test('CLI error formatter preserves diagnostic data for pairing failures', () => {
|
|
101
|
-
const error = new Error('mqttUrl is missing from pairing payload');
|
|
102
|
-
error.data = {
|
|
103
|
-
code: 'PAIR_CHANNEL_CONFIG_INVALID',
|
|
104
|
-
debug: {
|
|
105
|
-
rootKeys: ['channel'],
|
|
106
|
-
channelConfigKeys: ['mqtt_topic'],
|
|
107
|
-
hasExistingMqttUrl: false
|
|
108
|
-
}
|
|
109
|
-
};
|
|
110
|
-
assert.deepEqual(formatCliErrorPayload(error), {
|
|
111
|
-
ok: false,
|
|
112
|
-
error: {
|
|
113
|
-
message: 'mqttUrl is missing from pairing payload',
|
|
114
|
-
code: 'PAIR_CHANNEL_CONFIG_INVALID',
|
|
115
|
-
debug: {
|
|
116
|
-
rootKeys: ['channel'],
|
|
117
|
-
channelConfigKeys: ['mqtt_topic'],
|
|
118
|
-
hasExistingMqttUrl: false
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
});
|
|
122
|
-
});
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { describe, test } from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import fs from 'node:fs/promises';
|
|
4
|
-
import os from 'node:os';
|
|
5
|
-
import path from 'node:path';
|
|
6
|
-
import { resolveOpenClawBin } from './openclaw-bin.js';
|
|
7
|
-
describe('resolveOpenClawBin', () => {
|
|
8
|
-
test('uses APPDATA npm shim on Windows when OPENCLAW_BIN is unset', { skip: process.platform !== 'win32' }, async () => {
|
|
9
|
-
const originalOpenClawBin = process.env.OPENCLAW_BIN;
|
|
10
|
-
const originalAppData = process.env.APPDATA;
|
|
11
|
-
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'openclaw-bin-'));
|
|
12
|
-
try {
|
|
13
|
-
const appData = path.join(root, 'Roaming');
|
|
14
|
-
const shim = path.join(appData, 'npm', 'openclaw.cmd');
|
|
15
|
-
await fs.mkdir(path.dirname(shim), { recursive: true });
|
|
16
|
-
await fs.writeFile(shim, '@echo off\r\n', 'utf8');
|
|
17
|
-
delete process.env.OPENCLAW_BIN;
|
|
18
|
-
process.env.APPDATA = appData;
|
|
19
|
-
assert.equal(resolveOpenClawBin(), shim);
|
|
20
|
-
}
|
|
21
|
-
finally {
|
|
22
|
-
if (originalOpenClawBin === undefined) {
|
|
23
|
-
delete process.env.OPENCLAW_BIN;
|
|
24
|
-
}
|
|
25
|
-
else {
|
|
26
|
-
process.env.OPENCLAW_BIN = originalOpenClawBin;
|
|
27
|
-
}
|
|
28
|
-
if (originalAppData === undefined) {
|
|
29
|
-
delete process.env.APPDATA;
|
|
30
|
-
}
|
|
31
|
-
else {
|
|
32
|
-
process.env.APPDATA = originalAppData;
|
|
33
|
-
}
|
|
34
|
-
await fs.rm(root, { recursive: true, force: true });
|
|
35
|
-
}
|
|
36
|
-
});
|
|
37
|
-
});
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import fs from 'node:fs/promises';
|
|
3
|
-
import os from 'node:os';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import test from 'node:test';
|
|
6
|
-
import { setApiCoreBotConfig } from './admin.js';
|
|
7
|
-
test('setApiCoreBotConfig writes apiCoreBot endpoint without changing pairing or channel config', async () => {
|
|
8
|
-
const context = await createMethodContext();
|
|
9
|
-
const configPath = path.join(context.openclawRoot, 'openclaw.json');
|
|
10
|
-
await fs.writeFile(configPath, JSON.stringify({
|
|
11
|
-
plugins: {
|
|
12
|
-
entries: {
|
|
13
|
-
'rol-websocket-channel': {
|
|
14
|
-
enabled: true,
|
|
15
|
-
config: {
|
|
16
|
-
pairing: {
|
|
17
|
-
paired: true,
|
|
18
|
-
pairingKeyLast4: '7a46'
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
},
|
|
24
|
-
channels: {
|
|
25
|
-
'rol-websocket-channel': {
|
|
26
|
-
enabled: true,
|
|
27
|
-
config: {
|
|
28
|
-
mqttUrl: 'ws://mqtt.example.test:8083/mqtt'
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
}, null, 2));
|
|
33
|
-
const result = await setApiCoreBotConfig({
|
|
34
|
-
baseUrl: 'https://api.example.test/',
|
|
35
|
-
authToken: 'secret-token'
|
|
36
|
-
}, context);
|
|
37
|
-
const savedConfig = JSON.parse(await fs.readFile(configPath, 'utf8'));
|
|
38
|
-
assert.equal(result.ok, true);
|
|
39
|
-
assert.equal(result.apiCoreBot.baseUrl, 'https://api.example.test');
|
|
40
|
-
assert.equal(result.apiCoreBot.authToken, 'secr***oken');
|
|
41
|
-
assert.equal(savedConfig.plugins.entries['rol-websocket-channel'].config.apiCoreBot.baseUrl, 'https://api.example.test');
|
|
42
|
-
assert.equal(savedConfig.plugins.entries['rol-websocket-channel'].config.apiCoreBot.authToken, 'secret-token');
|
|
43
|
-
assert.equal(savedConfig.plugins.entries['rol-websocket-channel'].config.pairing.pairingKeyLast4, '7a46');
|
|
44
|
-
assert.equal(savedConfig.channels['rol-websocket-channel'].config.mqttUrl, 'ws://mqtt.example.test:8083/mqtt');
|
|
45
|
-
});
|
|
46
|
-
test('setApiCoreBotConfig rejects missing baseUrl instead of writing defaults', async () => {
|
|
47
|
-
const context = await createMethodContext();
|
|
48
|
-
const configPath = path.join(context.openclawRoot, 'openclaw.json');
|
|
49
|
-
await fs.writeFile(configPath, '{}');
|
|
50
|
-
await assert.rejects(() => setApiCoreBotConfig({}, context), /apiCoreBot\.baseUrl is required/);
|
|
51
|
-
assert.equal(await fs.readFile(configPath, 'utf8'), '{}');
|
|
52
|
-
});
|
|
53
|
-
async function createMethodContext() {
|
|
54
|
-
const projectRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'api-core-bot-config-test-'));
|
|
55
|
-
const openclawRoot = path.join(projectRoot, '.openclaw');
|
|
56
|
-
await fs.mkdir(openclawRoot, { recursive: true });
|
|
57
|
-
return {
|
|
58
|
-
projectRoot,
|
|
59
|
-
openclawRoot
|
|
60
|
-
};
|
|
61
|
-
}
|
|
@@ -1,304 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, test } from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import fs from 'node:fs/promises';
|
|
4
|
-
import os from 'node:os';
|
|
5
|
-
import path from 'node:path';
|
|
6
|
-
import * as artifactMethods from './artifacts.js';
|
|
7
|
-
import { ensureArtifactUploaded, listArtifacts, markArtifactUploaded, refreshArtifacts } from './artifacts.js';
|
|
8
|
-
const tempDirs = [];
|
|
9
|
-
const originalFetch = globalThis.fetch;
|
|
10
|
-
const optionalArtifactMethods = artifactMethods;
|
|
11
|
-
afterEach(async () => {
|
|
12
|
-
globalThis.fetch = originalFetch;
|
|
13
|
-
while (tempDirs.length > 0) {
|
|
14
|
-
const dir = tempDirs.pop();
|
|
15
|
-
if (dir) {
|
|
16
|
-
await fs.rm(dir, { recursive: true, force: true });
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
});
|
|
20
|
-
describe('artifacts workspace scope', () => {
|
|
21
|
-
test('only indexes allowed artifact types inside workspace', async () => {
|
|
22
|
-
const context = await createMethodContext();
|
|
23
|
-
const workspaceRoot = path.join(context.openclawRoot, 'workspace');
|
|
24
|
-
await fs.writeFile(path.join(workspaceRoot, 'SOUL.md'), '# root noise\n', 'utf8');
|
|
25
|
-
await fs.mkdir(path.join(workspaceRoot, 'exports'), { recursive: true });
|
|
26
|
-
await fs.mkdir(path.join(workspaceRoot, 'logs'), { recursive: true });
|
|
27
|
-
await fs.mkdir(path.join(workspaceRoot, 'nested', 'media'), { recursive: true });
|
|
28
|
-
await fs.writeFile(path.join(workspaceRoot, 'exports', 'preview.png'), 'png-data', 'utf8');
|
|
29
|
-
await fs.writeFile(path.join(workspaceRoot, 'exports', 'report.pdf'), 'pdf-data', 'utf8');
|
|
30
|
-
await fs.writeFile(path.join(workspaceRoot, 'nested', 'media', 'archive.zip'), 'zip-data', 'utf8');
|
|
31
|
-
await fs.writeFile(path.join(workspaceRoot, 'nested', 'media', 'video.mp4'), 'mp4-data', 'utf8');
|
|
32
|
-
await fs.writeFile(path.join(workspaceRoot, 'exports', 'spec.docx'), 'docx-data', 'utf8');
|
|
33
|
-
await fs.writeFile(path.join(workspaceRoot, 'exports', 'ignored.md'), '# markdown noise\n', 'utf8');
|
|
34
|
-
await fs.writeFile(path.join(workspaceRoot, 'exports', 'ignored.json'), '{"noise":true}\n', 'utf8');
|
|
35
|
-
await fs.writeFile(path.join(workspaceRoot, 'logs', 'ignored.pdf'), 'log pdf noise', 'utf8');
|
|
36
|
-
await fs.writeFile(path.join(workspaceRoot, 'artifacts.json'), '[]', 'utf8');
|
|
37
|
-
const result = await refreshArtifacts({}, context);
|
|
38
|
-
assert.equal(result.scope, 'workspace');
|
|
39
|
-
assert.equal(result.count, 5);
|
|
40
|
-
assert.deepEqual(result.items.map((item) => item.relativePath), [
|
|
41
|
-
'nested/media/archive.zip',
|
|
42
|
-
'exports/preview.png',
|
|
43
|
-
'exports/report.pdf',
|
|
44
|
-
'exports/spec.docx',
|
|
45
|
-
'nested/media/video.mp4'
|
|
46
|
-
]);
|
|
47
|
-
assert.equal(result.workspaceRoot, workspaceRoot);
|
|
48
|
-
assert.equal(result.manifestPath, path.join(workspaceRoot, 'artifacts.json'));
|
|
49
|
-
});
|
|
50
|
-
test('indexes only markdown files added after the initial baseline', async () => {
|
|
51
|
-
const context = await createMethodContext();
|
|
52
|
-
const workspaceRoot = path.join(context.openclawRoot, 'workspace');
|
|
53
|
-
await fs.mkdir(path.join(workspaceRoot, 'docs'), { recursive: true });
|
|
54
|
-
await fs.writeFile(path.join(workspaceRoot, 'docs', 'existing.md'), '# existing\n', 'utf8');
|
|
55
|
-
await fs.writeFile(path.join(workspaceRoot, 'MEMORY.md'), '# memory\n', 'utf8');
|
|
56
|
-
await fs.writeFile(path.join(workspaceRoot, 'SoUL.md'), '# soul\n', 'utf8');
|
|
57
|
-
const baseline = await refreshArtifacts({}, context);
|
|
58
|
-
assert.equal(baseline.count, 0);
|
|
59
|
-
assert.deepEqual(baseline.items, []);
|
|
60
|
-
await fs.writeFile(path.join(workspaceRoot, 'docs', 'existing.md'), '# changed\n', 'utf8');
|
|
61
|
-
await fs.writeFile(path.join(workspaceRoot, 'MEMORY.md'), '# changed memory\n', 'utf8');
|
|
62
|
-
await fs.writeFile(path.join(workspaceRoot, 'docs', 'generated.md'), '# generated\n', 'utf8');
|
|
63
|
-
const refreshed = await refreshArtifacts({}, context);
|
|
64
|
-
assert.equal(refreshed.count, 1);
|
|
65
|
-
assert.deepEqual(refreshed.items.map((item) => item.relativePath), ['docs/generated.md']);
|
|
66
|
-
assert.equal(refreshed.items[0]?.category, 'document');
|
|
67
|
-
assert.equal(refreshed.items[0]?.mimeType, 'text/markdown');
|
|
68
|
-
});
|
|
69
|
-
test('list reads workspace manifest without taskId', async () => {
|
|
70
|
-
const context = await createMethodContext();
|
|
71
|
-
const workspaceRoot = path.join(context.openclawRoot, 'workspace');
|
|
72
|
-
await fs.mkdir(path.join(workspaceRoot, 'exports'), { recursive: true });
|
|
73
|
-
await fs.writeFile(path.join(workspaceRoot, 'exports', 'me.pdf'), 'pdf-data', 'utf8');
|
|
74
|
-
const refreshed = await refreshArtifacts({}, context);
|
|
75
|
-
assert.equal(refreshed.count, 1);
|
|
76
|
-
const listed = await listArtifacts({ refresh: false }, context);
|
|
77
|
-
assert.equal(listed.scope, 'workspace');
|
|
78
|
-
assert.equal(listed.count, 1);
|
|
79
|
-
assert.deepEqual(listed.items.map((item) => item.relativePath), ['exports/me.pdf']);
|
|
80
|
-
assert.equal(listed.manifestPath, path.join(workspaceRoot, 'artifacts.json'));
|
|
81
|
-
assert.equal(listed.workspaceRoot, workspaceRoot);
|
|
82
|
-
});
|
|
83
|
-
test('ensureUploaded uploads local artifact and updates manifest', async () => {
|
|
84
|
-
const context = await createMethodContext();
|
|
85
|
-
const workspaceRoot = path.join(context.openclawRoot, 'workspace');
|
|
86
|
-
const artifactPath = path.join(workspaceRoot, 'exports', 'me.pdf');
|
|
87
|
-
await writeApiCoreBotConfig(context.openclawRoot, 'https://api.example.com');
|
|
88
|
-
await fs.mkdir(path.dirname(artifactPath), { recursive: true });
|
|
89
|
-
await fs.writeFile(artifactPath, 'pdf-data', 'utf8');
|
|
90
|
-
const refreshed = await refreshArtifacts({}, context);
|
|
91
|
-
const artifactId = refreshed.items[0]?.id;
|
|
92
|
-
assert.ok(artifactId);
|
|
93
|
-
const calls = [];
|
|
94
|
-
globalThis.fetch = (async (input, init) => {
|
|
95
|
-
const url = typeof input === 'string'
|
|
96
|
-
? input
|
|
97
|
-
: input instanceof URL
|
|
98
|
-
? input.toString()
|
|
99
|
-
: input.url;
|
|
100
|
-
const method = init?.method ?? (input instanceof Request ? input.method : 'GET');
|
|
101
|
-
calls.push({ url, method });
|
|
102
|
-
if (url === 'https://api.example.com/api-core-bot/front/s3/get-presigned-post') {
|
|
103
|
-
assert.equal(init?.body !== undefined, true);
|
|
104
|
-
const parsedBody = JSON.parse(String(init?.body));
|
|
105
|
-
assert.equal(parsedBody.dir, 'artifacts/');
|
|
106
|
-
assert.equal(parsedBody.filename, 'me.pdf');
|
|
107
|
-
assert.equal(parsedBody.file_name, undefined);
|
|
108
|
-
return new Response(JSON.stringify({
|
|
109
|
-
code: 0,
|
|
110
|
-
message: '',
|
|
111
|
-
success: true,
|
|
112
|
-
data: {
|
|
113
|
-
url: 'https://upload.example.com',
|
|
114
|
-
fields: {
|
|
115
|
-
key: 'artifacts/me.pdf',
|
|
116
|
-
policy: 'policy-token'
|
|
117
|
-
},
|
|
118
|
-
file_key: 'artifacts/me.pdf',
|
|
119
|
-
file_url: 'https://cdn.example.com/artifacts/me.pdf'
|
|
120
|
-
}
|
|
121
|
-
}), {
|
|
122
|
-
status: 200,
|
|
123
|
-
headers: { 'Content-Type': 'application/json' }
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
if (url === 'https://upload.example.com') {
|
|
127
|
-
assert.ok(init?.body instanceof FormData);
|
|
128
|
-
return new Response(null, { status: 204 });
|
|
129
|
-
}
|
|
130
|
-
throw new Error(`Unexpected fetch call: ${url}`);
|
|
131
|
-
});
|
|
132
|
-
const ensured = await ensureArtifactUploaded({
|
|
133
|
-
artifactId,
|
|
134
|
-
presignedPostBody: {
|
|
135
|
-
dir: 'artifacts/'
|
|
136
|
-
}
|
|
137
|
-
}, context);
|
|
138
|
-
assert.equal(ensured.ok, true);
|
|
139
|
-
assert.equal(ensured.uploaded, true);
|
|
140
|
-
assert.equal(ensured.downloadUrl, 'https://cdn.example.com/artifacts/me.pdf');
|
|
141
|
-
assert.equal(ensured.objectKey, 'artifacts/me.pdf');
|
|
142
|
-
assert.equal(ensured.item.storageStatus, 'uploaded');
|
|
143
|
-
assert.equal(ensured.item.fileUrl, 'https://cdn.example.com/artifacts/me.pdf');
|
|
144
|
-
assert.equal(ensured.item.objectKey, 'artifacts/me.pdf');
|
|
145
|
-
assert.deepEqual(calls, [
|
|
146
|
-
{
|
|
147
|
-
url: 'https://api.example.com/api-core-bot/front/s3/get-presigned-post',
|
|
148
|
-
method: 'POST'
|
|
149
|
-
},
|
|
150
|
-
{
|
|
151
|
-
url: 'https://upload.example.com',
|
|
152
|
-
method: 'POST'
|
|
153
|
-
}
|
|
154
|
-
]);
|
|
155
|
-
const listed = await listArtifacts({ refresh: false }, context);
|
|
156
|
-
assert.equal(listed.items.length, 1);
|
|
157
|
-
assert.equal(listed.items[0]?.relativePath, 'exports/me.pdf');
|
|
158
|
-
assert.equal(listed.items[0]?.storageStatus, 'uploaded');
|
|
159
|
-
assert.equal(listed.items[0]?.fileUrl, 'https://cdn.example.com/artifacts/me.pdf');
|
|
160
|
-
assert.equal(listed.items[0]?.objectKey, 'artifacts/me.pdf');
|
|
161
|
-
});
|
|
162
|
-
test('ensureUploaded reuses existing uploaded artifact without fetching', async () => {
|
|
163
|
-
const context = await createMethodContext();
|
|
164
|
-
const workspaceRoot = path.join(context.openclawRoot, 'workspace');
|
|
165
|
-
const artifactPath = path.join(workspaceRoot, 'exports', 'me.pdf');
|
|
166
|
-
await fs.mkdir(path.dirname(artifactPath), { recursive: true });
|
|
167
|
-
await fs.writeFile(artifactPath, 'pdf-data', 'utf8');
|
|
168
|
-
const refreshed = await refreshArtifacts({}, context);
|
|
169
|
-
const artifactId = refreshed.items[0]?.id;
|
|
170
|
-
assert.ok(artifactId);
|
|
171
|
-
await markArtifactUploaded({
|
|
172
|
-
artifactId,
|
|
173
|
-
objectKey: 'artifacts/me.pdf',
|
|
174
|
-
fileUrl: 'https://cdn.example.com/artifacts/me.pdf'
|
|
175
|
-
}, context);
|
|
176
|
-
globalThis.fetch = (async () => {
|
|
177
|
-
throw new Error('fetch should not be called for existing uploaded artifact');
|
|
178
|
-
});
|
|
179
|
-
const ensured = await ensureArtifactUploaded({ artifactId }, context);
|
|
180
|
-
assert.equal(ensured.ok, true);
|
|
181
|
-
assert.equal(ensured.uploaded, false);
|
|
182
|
-
assert.equal(ensured.downloadUrl, 'https://cdn.example.com/artifacts/me.pdf');
|
|
183
|
-
assert.equal(ensured.item.storageStatus, 'uploaded');
|
|
184
|
-
assert.equal(ensured.item.fileUrl, 'https://cdn.example.com/artifacts/me.pdf');
|
|
185
|
-
});
|
|
186
|
-
test('findLatest returns newest matching artifacts with filters', async () => {
|
|
187
|
-
const findLatestArtifacts = optionalArtifactMethods.findLatestArtifacts;
|
|
188
|
-
if (!findLatestArtifacts) {
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
const context = await createMethodContext();
|
|
192
|
-
const workspaceRoot = path.join(context.openclawRoot, 'workspace');
|
|
193
|
-
await fs.mkdir(path.join(workspaceRoot, 'exports'), { recursive: true });
|
|
194
|
-
await fs.writeFile(path.join(workspaceRoot, 'exports', 'first.pdf'), 'pdf-data', 'utf8');
|
|
195
|
-
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
196
|
-
await fs.writeFile(path.join(workspaceRoot, 'exports', 'second.png'), 'png-data', 'utf8');
|
|
197
|
-
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
198
|
-
await fs.writeFile(path.join(workspaceRoot, 'exports', 'third.docx'), 'docx-data', 'utf8');
|
|
199
|
-
const latest = await findLatestArtifacts({
|
|
200
|
-
category: 'document',
|
|
201
|
-
relativePathPrefix: 'exports/',
|
|
202
|
-
limit: 2
|
|
203
|
-
}, context);
|
|
204
|
-
assert.equal(latest.ok, true);
|
|
205
|
-
assert.equal(latest.count, 2);
|
|
206
|
-
assert.deepEqual(latest.items.map((item) => item.fileName), ['third.docx', 'first.pdf']);
|
|
207
|
-
assert.deepEqual(latest.items.map((item) => item.category), ['document', 'document']);
|
|
208
|
-
assert.deepEqual(latest.items.map((item) => item.relativePath), ['exports/third.docx', 'exports/first.pdf']);
|
|
209
|
-
});
|
|
210
|
-
test('publish refreshes manifest for a new file and uploads it by relativePath', async () => {
|
|
211
|
-
const publishArtifact = optionalArtifactMethods.publishArtifact;
|
|
212
|
-
if (!publishArtifact) {
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
const context = await createMethodContext();
|
|
216
|
-
const workspaceRoot = path.join(context.openclawRoot, 'workspace');
|
|
217
|
-
const artifactPath = path.join(workspaceRoot, 'generated', 'result.pdf');
|
|
218
|
-
await writeApiCoreBotConfig(context.openclawRoot, 'https://api.example.com');
|
|
219
|
-
await fs.mkdir(path.dirname(artifactPath), { recursive: true });
|
|
220
|
-
await fs.writeFile(artifactPath, 'pdf-data', 'utf8');
|
|
221
|
-
const calls = [];
|
|
222
|
-
globalThis.fetch = (async (input, init) => {
|
|
223
|
-
const url = typeof input === 'string'
|
|
224
|
-
? input
|
|
225
|
-
: input instanceof URL
|
|
226
|
-
? input.toString()
|
|
227
|
-
: input.url;
|
|
228
|
-
const method = init?.method ?? (input instanceof Request ? input.method : 'GET');
|
|
229
|
-
calls.push({ url, method });
|
|
230
|
-
if (url === 'https://api.example.com/api-core-bot/front/s3/get-presigned-post') {
|
|
231
|
-
const parsedBody = JSON.parse(String(init?.body));
|
|
232
|
-
assert.equal(parsedBody.filename, 'result.pdf');
|
|
233
|
-
assert.equal(parsedBody.dir, 'generated/');
|
|
234
|
-
return new Response(JSON.stringify({
|
|
235
|
-
success: true,
|
|
236
|
-
data: {
|
|
237
|
-
url: 'https://upload.example.com',
|
|
238
|
-
fields: {
|
|
239
|
-
key: 'generated/result.pdf',
|
|
240
|
-
policy: 'policy-token'
|
|
241
|
-
},
|
|
242
|
-
file_url: 'https://cdn.example.com/generated/result.pdf'
|
|
243
|
-
}
|
|
244
|
-
}), {
|
|
245
|
-
status: 200,
|
|
246
|
-
headers: { 'Content-Type': 'application/json' }
|
|
247
|
-
});
|
|
248
|
-
}
|
|
249
|
-
if (url === 'https://upload.example.com') {
|
|
250
|
-
assert.ok(init?.body instanceof FormData);
|
|
251
|
-
return new Response(null, { status: 204 });
|
|
252
|
-
}
|
|
253
|
-
throw new Error(`Unexpected fetch call: ${url}`);
|
|
254
|
-
});
|
|
255
|
-
const published = await publishArtifact({
|
|
256
|
-
relativePath: 'generated/result.pdf',
|
|
257
|
-
presignedPostBody: {
|
|
258
|
-
dir: 'generated/'
|
|
259
|
-
}
|
|
260
|
-
}, context);
|
|
261
|
-
assert.equal(published.ok, true);
|
|
262
|
-
assert.equal(published.uploaded, true);
|
|
263
|
-
assert.equal(published.downloadUrl, 'https://cdn.example.com/generated/result.pdf');
|
|
264
|
-
assert.equal(published.item.relativePath, 'generated/result.pdf');
|
|
265
|
-
assert.equal(published.item.storageStatus, 'uploaded');
|
|
266
|
-
assert.match(published.artifactId, /^art_/);
|
|
267
|
-
assert.deepEqual(calls, [
|
|
268
|
-
{
|
|
269
|
-
url: 'https://api.example.com/api-core-bot/front/s3/get-presigned-post',
|
|
270
|
-
method: 'POST'
|
|
271
|
-
},
|
|
272
|
-
{
|
|
273
|
-
url: 'https://upload.example.com',
|
|
274
|
-
method: 'POST'
|
|
275
|
-
}
|
|
276
|
-
]);
|
|
277
|
-
});
|
|
278
|
-
});
|
|
279
|
-
async function createMethodContext() {
|
|
280
|
-
const rootDir = await fs.mkdtemp(path.join(os.tmpdir(), 'artifacts-task-scope-'));
|
|
281
|
-
tempDirs.push(rootDir);
|
|
282
|
-
const openclawRoot = path.join(rootDir, '.openclaw');
|
|
283
|
-
const workspaceRoot = path.join(openclawRoot, 'workspace');
|
|
284
|
-
await fs.mkdir(workspaceRoot, { recursive: true });
|
|
285
|
-
return {
|
|
286
|
-
projectRoot: rootDir,
|
|
287
|
-
openclawRoot
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
|
-
async function writeApiCoreBotConfig(openclawRoot, baseUrl) {
|
|
291
|
-
await fs.writeFile(path.join(openclawRoot, 'openclaw.json'), JSON.stringify({
|
|
292
|
-
plugins: {
|
|
293
|
-
entries: {
|
|
294
|
-
'rol-websocket-channel': {
|
|
295
|
-
config: {
|
|
296
|
-
apiCoreBot: {
|
|
297
|
-
baseUrl
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
}, null, 2), 'utf8');
|
|
304
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import test from 'node:test';
|
|
3
|
-
import * as artifactMethods from './artifacts.js';
|
|
4
|
-
import { setApiCoreBotConfig } from './admin.js';
|
|
5
|
-
import { getMethod, listMethods } from './index.js';
|
|
6
|
-
import { currentVersion } from './system.js';
|
|
7
|
-
test('methods registry exposes local artifact discovery and publish methods', () => {
|
|
8
|
-
const optionalArtifactMethods = artifactMethods;
|
|
9
|
-
if (optionalArtifactMethods.findLatestArtifacts) {
|
|
10
|
-
assert.equal(getMethod('artifacts.findLatest'), optionalArtifactMethods.findLatestArtifacts);
|
|
11
|
-
assert.ok(listMethods().includes('artifacts.findLatest'));
|
|
12
|
-
}
|
|
13
|
-
if (optionalArtifactMethods.publishArtifact) {
|
|
14
|
-
assert.equal(getMethod('artifacts.publish'), optionalArtifactMethods.publishArtifact);
|
|
15
|
-
assert.ok(listMethods().includes('artifacts.publish'));
|
|
16
|
-
}
|
|
17
|
-
});
|
|
18
|
-
test('methods registry exposes apiCoreBot config writer', () => {
|
|
19
|
-
assert.equal(getMethod('config.setApiCoreBot'), setApiCoreBotConfig);
|
|
20
|
-
assert.ok(listMethods().includes('config.setApiCoreBot'));
|
|
21
|
-
});
|
|
22
|
-
test('methods registry exposes current version method', () => {
|
|
23
|
-
assert.equal(getMethod('system.currentVersion'), currentVersion);
|
|
24
|
-
assert.ok(listMethods().includes('system.currentVersion'));
|
|
25
|
-
assert.ok(!listMethods().includes('system.ensureLatest'));
|
|
26
|
-
assert.ok(!listMethods().includes('config.setVersionManagement'));
|
|
27
|
-
});
|