rol-websocket-channel 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MQTT-API.md +967 -0
- package/dist/index.js +430 -0
- package/dist/message-handler.js +327 -0
- package/dist/src/admin/cli.js +43 -0
- package/dist/src/admin/jsonrpc.js +60 -0
- package/dist/src/admin/lib/fs.js +30 -0
- package/dist/src/admin/lib/paths.js +46 -0
- package/dist/src/admin/methods/admin.js +60 -0
- package/dist/src/admin/methods/agents-extended.js +235 -0
- package/dist/src/admin/methods/index.js +69 -0
- package/dist/src/admin/methods/memory.js +360 -0
- package/dist/src/admin/methods/models-extended.js +107 -0
- package/dist/src/admin/methods/models.js +39 -0
- package/dist/src/admin/methods/sessions-extended.js +207 -0
- package/dist/src/admin/methods/sessions.js +64 -0
- package/dist/src/admin/methods/skills-extended.js +157 -0
- package/dist/src/admin/methods/skills-toggle.js +182 -0
- package/dist/src/admin/methods/skills.js +384 -0
- package/dist/src/admin/methods/system.js +178 -0
- package/dist/src/admin/methods/usage.js +1170 -0
- package/dist/src/admin/types.js +1 -0
- package/dist/src/mqtt/connection-manager.js +155 -0
- package/dist/src/mqtt/index.js +5 -0
- package/dist/src/mqtt/mqtt-client.js +86 -0
- package/dist/src/mqtt/types.js +2 -0
- package/dist/src/shared/context.js +24 -0
- package/dist/src/shared/wrapper.js +23 -0
- package/index.ts +514 -0
- package/message-handler.ts +415 -0
- package/openclaw.plugin.json +84 -0
- package/package.json +35 -0
- package/readme.md +32 -0
- package/src/admin/cli.ts +60 -0
- package/src/admin/jsonrpc.ts +88 -0
- package/src/admin/lib/fs.ts +35 -0
- package/src/admin/lib/paths.ts +61 -0
- package/src/admin/methods/admin.ts +95 -0
- package/src/admin/methods/agents-extended.ts +310 -0
- package/src/admin/methods/index.ts +103 -0
- package/src/admin/methods/memory.ts +546 -0
- package/src/admin/methods/models-extended.ts +191 -0
- package/src/admin/methods/models.ts +103 -0
- package/src/admin/methods/sessions-extended.ts +313 -0
- package/src/admin/methods/sessions.ts +122 -0
- package/src/admin/methods/skills-extended.ts +249 -0
- package/src/admin/methods/skills-toggle.ts +235 -0
- package/src/admin/methods/skills.ts +651 -0
- package/src/admin/methods/system.ts +203 -0
- package/src/admin/methods/usage.ts +1491 -0
- package/src/admin/types.ts +46 -0
- package/src/mqtt/connection-manager.ts +188 -0
- package/src/mqtt/index.ts +6 -0
- package/src/mqtt/mqtt-client.ts +119 -0
- package/src/mqtt/types.ts +36 -0
- package/src/shared/context.ts +33 -0
- package/src/shared/wrapper.ts +35 -0
- package/tsconfig.json +16 -0
- package/types/openclaw.d.ts +74 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { pathExists, readJsonFile, writeJsonFile } from '../lib/fs.ts';
|
|
4
|
+
import { JsonRpcException, JSON_RPC_ERRORS } from '../jsonrpc.ts';
|
|
5
|
+
const ALLOWED_AGENT_UPDATES = {
|
|
6
|
+
'name': true,
|
|
7
|
+
'workspace': true,
|
|
8
|
+
'agentDir': true,
|
|
9
|
+
'model.primary': true,
|
|
10
|
+
'model.provider': true,
|
|
11
|
+
'skills': true,
|
|
12
|
+
'tools.profile': true,
|
|
13
|
+
'behavior': true,
|
|
14
|
+
};
|
|
15
|
+
export const listAgents = async (_params, context) => {
|
|
16
|
+
const configPath = path.join(context.openclawRoot, 'openclaw.json');
|
|
17
|
+
const config = await loadConfig(context.openclawRoot);
|
|
18
|
+
const items = normalizeAgentList(config);
|
|
19
|
+
return {
|
|
20
|
+
sourceConfigFile: configPath,
|
|
21
|
+
count: items.length,
|
|
22
|
+
items
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
export const createAgent = async (params, context) => {
|
|
26
|
+
const objectParams = expectObject(params);
|
|
27
|
+
const rawAgentId = typeof objectParams.agentId === 'string'
|
|
28
|
+
? objectParams.agentId
|
|
29
|
+
: typeof objectParams.id === 'string'
|
|
30
|
+
? objectParams.id
|
|
31
|
+
: undefined;
|
|
32
|
+
const agentId = sanitizeAgentId(expectString(rawAgentId, 'agentId'));
|
|
33
|
+
const configPath = path.join(context.openclawRoot, 'openclaw.json');
|
|
34
|
+
const config = await loadConfig(context.openclawRoot);
|
|
35
|
+
const agents = ensureAgentList(config);
|
|
36
|
+
if (findNamedAgent(agents, agentId)) {
|
|
37
|
+
throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, `Agent already exists: ${agentId}`);
|
|
38
|
+
}
|
|
39
|
+
const workspace = typeof objectParams.workspace === 'string' && objectParams.workspace.trim().length > 0
|
|
40
|
+
? objectParams.workspace.trim()
|
|
41
|
+
: path.join(context.openclawRoot, 'workspace', agentId);
|
|
42
|
+
const agentDir = typeof objectParams.agentDir === 'string' && objectParams.agentDir.trim().length > 0
|
|
43
|
+
? objectParams.agentDir.trim()
|
|
44
|
+
: path.join(context.openclawRoot, 'agents', agentId, 'agent');
|
|
45
|
+
const created = {
|
|
46
|
+
id: agentId,
|
|
47
|
+
name: typeof objectParams.name === 'string' && objectParams.name.trim().length > 0
|
|
48
|
+
? objectParams.name.trim()
|
|
49
|
+
: agentId,
|
|
50
|
+
workspace,
|
|
51
|
+
agentDir
|
|
52
|
+
};
|
|
53
|
+
if (typeof objectParams.modelPrimary === 'string' && objectParams.modelPrimary.trim().length > 0) {
|
|
54
|
+
created.model = { ...(created.model ?? {}), primary: objectParams.modelPrimary.trim() };
|
|
55
|
+
}
|
|
56
|
+
if (typeof objectParams.modelProvider === 'string' && objectParams.modelProvider.trim().length > 0) {
|
|
57
|
+
created.model = { ...(created.model ?? {}), provider: objectParams.modelProvider.trim() };
|
|
58
|
+
}
|
|
59
|
+
if (Array.isArray(objectParams.skills)) {
|
|
60
|
+
created.skills = objectParams.skills.filter((item) => typeof item === 'string');
|
|
61
|
+
}
|
|
62
|
+
if (typeof objectParams.toolsProfile === 'string' && objectParams.toolsProfile.trim().length > 0) {
|
|
63
|
+
created.tools = { ...(created.tools ?? {}), profile: objectParams.toolsProfile.trim() };
|
|
64
|
+
}
|
|
65
|
+
if (isObject(objectParams.behavior)) {
|
|
66
|
+
created.behavior = objectParams.behavior;
|
|
67
|
+
}
|
|
68
|
+
agents.push(created);
|
|
69
|
+
await fs.mkdir(workspace, { recursive: true });
|
|
70
|
+
await fs.mkdir(agentDir, { recursive: true });
|
|
71
|
+
await writeJsonFile(configPath, config);
|
|
72
|
+
return {
|
|
73
|
+
success: true,
|
|
74
|
+
configFile: configPath,
|
|
75
|
+
created
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
export const deleteAgent = async (params, context) => {
|
|
79
|
+
const objectParams = expectObject(params);
|
|
80
|
+
const rawAgentId = typeof objectParams.agentId === 'string'
|
|
81
|
+
? objectParams.agentId
|
|
82
|
+
: typeof objectParams.id === 'string'
|
|
83
|
+
? objectParams.id
|
|
84
|
+
: undefined;
|
|
85
|
+
const agentId = sanitizeAgentId(expectString(rawAgentId, 'agentId'));
|
|
86
|
+
const purgeFiles = objectParams.purgeFiles === true;
|
|
87
|
+
const configPath = path.join(context.openclawRoot, 'openclaw.json');
|
|
88
|
+
const config = await loadConfig(context.openclawRoot);
|
|
89
|
+
const agents = ensureAgentList(config);
|
|
90
|
+
const index = agents.findIndex((agent) => isMatchingAgent(agent, agentId));
|
|
91
|
+
if (index === -1) {
|
|
92
|
+
throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, `Agent not found: ${agentId}`);
|
|
93
|
+
}
|
|
94
|
+
const [removed] = agents.splice(index, 1);
|
|
95
|
+
await writeJsonFile(configPath, config);
|
|
96
|
+
const removedPaths = [];
|
|
97
|
+
if (purgeFiles) {
|
|
98
|
+
const workspaceRoot = path.normalize(path.join(context.openclawRoot, 'workspace'));
|
|
99
|
+
const agentsRoot = path.normalize(path.join(context.openclawRoot, 'agents'));
|
|
100
|
+
const purgeCandidates = [
|
|
101
|
+
typeof removed.workspace === 'string' ? removed.workspace : null,
|
|
102
|
+
typeof removed.agentDir === 'string' ? path.dirname(removed.agentDir) : null
|
|
103
|
+
].filter((value) => Boolean(value));
|
|
104
|
+
for (const candidate of purgeCandidates) {
|
|
105
|
+
const normalized = path.normalize(candidate);
|
|
106
|
+
const insideWorkspace = normalized === workspaceRoot || normalized.startsWith(`${workspaceRoot}${path.sep}`);
|
|
107
|
+
const insideAgents = normalized === agentsRoot || normalized.startsWith(`${agentsRoot}${path.sep}`);
|
|
108
|
+
if (!insideWorkspace && !insideAgents) {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (await pathExists(candidate)) {
|
|
112
|
+
await fs.rm(candidate, { recursive: true, force: true });
|
|
113
|
+
removedPaths.push(candidate);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
success: true,
|
|
119
|
+
configFile: configPath,
|
|
120
|
+
removed,
|
|
121
|
+
purgeFiles,
|
|
122
|
+
removedPaths
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
export const updateAgent = async (params, context) => {
|
|
126
|
+
const objectParams = isObject(params) ? params : {};
|
|
127
|
+
const updates = isObject(objectParams.updates) ? objectParams.updates : null;
|
|
128
|
+
const rawAgentId = typeof objectParams.agentId === 'string'
|
|
129
|
+
? objectParams.agentId
|
|
130
|
+
: typeof objectParams.agentName === 'string'
|
|
131
|
+
? objectParams.agentName
|
|
132
|
+
: typeof objectParams.id === 'string'
|
|
133
|
+
? objectParams.id
|
|
134
|
+
: 'defaults';
|
|
135
|
+
const agentId = typeof rawAgentId === 'string' && rawAgentId !== 'defaults'
|
|
136
|
+
? sanitizeAgentId(rawAgentId)
|
|
137
|
+
: rawAgentId;
|
|
138
|
+
if (!updates) {
|
|
139
|
+
throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, 'Missing required parameter: updates (object with field paths and values)');
|
|
140
|
+
}
|
|
141
|
+
const configPath = path.join(context.openclawRoot, 'openclaw.json');
|
|
142
|
+
const config = await loadConfig(context.openclawRoot);
|
|
143
|
+
const target = resolveAgentTarget(config, agentId);
|
|
144
|
+
const changes = [];
|
|
145
|
+
for (const [fieldPath, value] of Object.entries(updates)) {
|
|
146
|
+
if (!ALLOWED_AGENT_UPDATES[fieldPath]) {
|
|
147
|
+
throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, `Field not allowed for update: ${fieldPath}. Allowed fields: ${Object.keys(ALLOWED_AGENT_UPDATES).join(', ')}`);
|
|
148
|
+
}
|
|
149
|
+
setNestedValue(target, fieldPath, value);
|
|
150
|
+
changes.push(`Updated ${fieldPath} to: ${JSON.stringify(value)}`);
|
|
151
|
+
}
|
|
152
|
+
await writeJsonFile(configPath, config);
|
|
153
|
+
return {
|
|
154
|
+
success: true,
|
|
155
|
+
configFile: configPath,
|
|
156
|
+
agentId,
|
|
157
|
+
changes,
|
|
158
|
+
updatedConfig: target
|
|
159
|
+
};
|
|
160
|
+
};
|
|
161
|
+
function setNestedValue(obj, pathExpression, value) {
|
|
162
|
+
const keys = pathExpression.split('.');
|
|
163
|
+
let current = obj;
|
|
164
|
+
for (let index = 0; index < keys.length - 1; index += 1) {
|
|
165
|
+
const key = keys[index];
|
|
166
|
+
if (!current[key] || typeof current[key] !== 'object') {
|
|
167
|
+
current[key] = {};
|
|
168
|
+
}
|
|
169
|
+
current = current[key];
|
|
170
|
+
}
|
|
171
|
+
current[keys[keys.length - 1]] = value;
|
|
172
|
+
}
|
|
173
|
+
function isObject(value) {
|
|
174
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
175
|
+
}
|
|
176
|
+
function expectObject(value) {
|
|
177
|
+
if (!value || Array.isArray(value) || typeof value !== 'object') {
|
|
178
|
+
throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, 'Params must be an object');
|
|
179
|
+
}
|
|
180
|
+
return value;
|
|
181
|
+
}
|
|
182
|
+
function expectString(value, fieldName) {
|
|
183
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
184
|
+
throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, `Field '${fieldName}' must be a non-empty string`);
|
|
185
|
+
}
|
|
186
|
+
return value.trim();
|
|
187
|
+
}
|
|
188
|
+
async function loadConfig(openclawRoot) {
|
|
189
|
+
return await readJsonFile(path.join(openclawRoot, 'openclaw.json'));
|
|
190
|
+
}
|
|
191
|
+
function ensureAgentList(config) {
|
|
192
|
+
if (!config.agents)
|
|
193
|
+
config.agents = {};
|
|
194
|
+
if (!Array.isArray(config.agents.list))
|
|
195
|
+
config.agents.list = [];
|
|
196
|
+
return config.agents.list;
|
|
197
|
+
}
|
|
198
|
+
function normalizeAgentList(config) {
|
|
199
|
+
const defaults = {
|
|
200
|
+
id: 'defaults',
|
|
201
|
+
name: 'defaults',
|
|
202
|
+
...(config.agents?.defaults ?? {})
|
|
203
|
+
};
|
|
204
|
+
const named = Array.isArray(config.agents?.list) ? config.agents.list : [];
|
|
205
|
+
return [defaults, ...named];
|
|
206
|
+
}
|
|
207
|
+
function resolveAgentTarget(config, agentName) {
|
|
208
|
+
if (agentName === 'defaults') {
|
|
209
|
+
if (!config.agents)
|
|
210
|
+
config.agents = {};
|
|
211
|
+
if (!config.agents.defaults)
|
|
212
|
+
config.agents.defaults = {};
|
|
213
|
+
return config.agents.defaults;
|
|
214
|
+
}
|
|
215
|
+
const agents = ensureAgentList(config);
|
|
216
|
+
const found = findNamedAgent(agents, agentName);
|
|
217
|
+
if (!found) {
|
|
218
|
+
throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, `Agent not found: ${agentName}`);
|
|
219
|
+
}
|
|
220
|
+
return found;
|
|
221
|
+
}
|
|
222
|
+
function findNamedAgent(agents, agentName) {
|
|
223
|
+
return agents.find((agent) => isMatchingAgent(agent, agentName)) ?? null;
|
|
224
|
+
}
|
|
225
|
+
function isMatchingAgent(agent, agentName) {
|
|
226
|
+
return agent.id === agentName || agent.name === agentName;
|
|
227
|
+
}
|
|
228
|
+
function sanitizeAgentId(value) {
|
|
229
|
+
return value
|
|
230
|
+
.trim()
|
|
231
|
+
.replace(/[^a-zA-Z0-9._-]+/g, '-')
|
|
232
|
+
.replace(/-+/g, '-')
|
|
233
|
+
.replace(/^-|-$/g, '')
|
|
234
|
+
.toLowerCase();
|
|
235
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { JsonRpcException, JSON_RPC_ERRORS } from '../jsonrpc.ts';
|
|
2
|
+
import { getAgents, getConfig } from './admin.ts';
|
|
3
|
+
import { createAgent, deleteAgent, listAgents, updateAgent } from './agents-extended.ts';
|
|
4
|
+
import { backupMemory, createMemoryBackupRecord, exportMemoryZip, getMemoryPresignedPost, getMemoryFile, importMemoryZip, listMemoryFiles } from './memory.ts';
|
|
5
|
+
import { getModels } from './models.ts';
|
|
6
|
+
import { updateModels } from './models-extended.ts';
|
|
7
|
+
import { listSessions } from './sessions.ts';
|
|
8
|
+
import { getSession, prepareMessage, attachSkill } from './sessions-extended.ts';
|
|
9
|
+
import { installSkillFromNpm, listInstalledSkills } from './skills.ts';
|
|
10
|
+
import { getInstalledSkill, uninstallSkill } from './skills-extended.ts';
|
|
11
|
+
import { ping, restart, stop, doctorFix, logs } from './system.ts';
|
|
12
|
+
import { getUsageBreakdown, getUsagePageSummary, getUsageSummary, getUsageTimeseries } from './usage.ts';
|
|
13
|
+
const methods = new Map([
|
|
14
|
+
// System
|
|
15
|
+
['system.ping', ping],
|
|
16
|
+
['system.restart', restart],
|
|
17
|
+
['system.stop', stop],
|
|
18
|
+
['system.doctorFix', doctorFix],
|
|
19
|
+
['system.logs', logs],
|
|
20
|
+
// Agents
|
|
21
|
+
['agents.get', getAgents],
|
|
22
|
+
['agents.list', listAgents],
|
|
23
|
+
['agents.create', createAgent],
|
|
24
|
+
['agents.delete', deleteAgent],
|
|
25
|
+
['agents.update', updateAgent],
|
|
26
|
+
// Config
|
|
27
|
+
['config.get', getConfig],
|
|
28
|
+
// Sessions
|
|
29
|
+
['sessions.list', listSessions],
|
|
30
|
+
['sessions.get', getSession],
|
|
31
|
+
['sessions.prepareMessage', prepareMessage],
|
|
32
|
+
['sessions.attachSkill', attachSkill],
|
|
33
|
+
// Models
|
|
34
|
+
['models.get', getModels],
|
|
35
|
+
['models.update', updateModels],
|
|
36
|
+
// Usage
|
|
37
|
+
['usage.summary', getUsageSummary],
|
|
38
|
+
['adminBridge.usagePageSummary', getUsagePageSummary],
|
|
39
|
+
['adminBridge.usageTimeseries', getUsageTimeseries],
|
|
40
|
+
['adminBridge.usageBreakdown', getUsageBreakdown],
|
|
41
|
+
['admin.usagePageSummary', getUsagePageSummary],
|
|
42
|
+
['admin.usageTimeseries', getUsageTimeseries],
|
|
43
|
+
['admin.usageBreakdown', getUsageBreakdown],
|
|
44
|
+
// Skills
|
|
45
|
+
['skills.listInstalled', listInstalledSkills],
|
|
46
|
+
['skills.installFromNpm', installSkillFromNpm],
|
|
47
|
+
['skills.getInstalled', getInstalledSkill],
|
|
48
|
+
['skills.uninstall', uninstallSkill],
|
|
49
|
+
// Memory
|
|
50
|
+
['memory.listFiles', listMemoryFiles],
|
|
51
|
+
['memory.getFile', getMemoryFile],
|
|
52
|
+
['memory.backup', backupMemory],
|
|
53
|
+
['memory.exportZip', exportMemoryZip],
|
|
54
|
+
['memory.getPresignedPost', getMemoryPresignedPost],
|
|
55
|
+
['memory.createBackupRecord', createMemoryBackupRecord],
|
|
56
|
+
['memory.importZip', importMemoryZip]
|
|
57
|
+
]);
|
|
58
|
+
export function getMethod(methodName) {
|
|
59
|
+
const handler = methods.get(methodName);
|
|
60
|
+
if (!handler) {
|
|
61
|
+
throw new JsonRpcException(JSON_RPC_ERRORS.methodNotFound, 'Method not found', {
|
|
62
|
+
method: methodName
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return handler;
|
|
66
|
+
}
|
|
67
|
+
export function listMethods() {
|
|
68
|
+
return [...methods.keys()];
|
|
69
|
+
}
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { execFile } from 'node:child_process';
|
|
5
|
+
import { promisify } from 'node:util';
|
|
6
|
+
import { copyIfExists, ensureDir, pathExists } from '../lib/fs.ts';
|
|
7
|
+
import { ensureInside } from '../lib/paths.ts';
|
|
8
|
+
import { JsonRpcException, JSON_RPC_ERRORS } from '../jsonrpc.ts';
|
|
9
|
+
const execFileAsync = promisify(execFile);
|
|
10
|
+
const WORKSPACE_ROOT_FILES = [
|
|
11
|
+
'AGENTS.md',
|
|
12
|
+
'HEARTBEAT.md',
|
|
13
|
+
'IDENTITY.md',
|
|
14
|
+
'MEMORY.md',
|
|
15
|
+
'SoUL.md',
|
|
16
|
+
'TooLS.md',
|
|
17
|
+
'USER.md'
|
|
18
|
+
];
|
|
19
|
+
export const listMemoryFiles = async (_params, context) => {
|
|
20
|
+
const workspaceDir = path.join(context.openclawRoot, 'workspace');
|
|
21
|
+
const items = [];
|
|
22
|
+
for (const fileName of WORKSPACE_ROOT_FILES) {
|
|
23
|
+
const filePath = path.join(workspaceDir, fileName);
|
|
24
|
+
if (!(await pathExists(filePath))) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
const stat = await fs.stat(filePath);
|
|
28
|
+
items.push({
|
|
29
|
+
path: `workspace/${fileName}`,
|
|
30
|
+
size: stat.size,
|
|
31
|
+
updatedAt: stat.mtime.toISOString()
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
const rootConfigPath = path.join(context.openclawRoot, 'openclaw.json');
|
|
35
|
+
if (await pathExists(rootConfigPath)) {
|
|
36
|
+
const stat = await fs.stat(rootConfigPath);
|
|
37
|
+
items.push({
|
|
38
|
+
path: 'openclaw.json',
|
|
39
|
+
size: stat.size,
|
|
40
|
+
updatedAt: stat.mtime.toISOString()
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
const credentialsDir = path.join(context.openclawRoot, 'credentials');
|
|
44
|
+
if (await pathExists(credentialsDir)) {
|
|
45
|
+
const credentialEntries = await fs.readdir(credentialsDir, { withFileTypes: true });
|
|
46
|
+
for (const entry of credentialEntries) {
|
|
47
|
+
if (!entry.isFile()) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const fullPath = path.join(credentialsDir, entry.name);
|
|
51
|
+
const stat = await fs.stat(fullPath);
|
|
52
|
+
items.push({
|
|
53
|
+
path: `credentials/${entry.name}`,
|
|
54
|
+
size: stat.size,
|
|
55
|
+
updatedAt: stat.mtime.toISOString()
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
count: items.length,
|
|
61
|
+
items
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
export const getMemoryFile = async (params, context) => {
|
|
65
|
+
const objectParams = expectObject(params);
|
|
66
|
+
const filePath = expectString(objectParams.path, 'path');
|
|
67
|
+
const resolved = resolveMemoryPath(context.openclawRoot, filePath);
|
|
68
|
+
const content = await fs.readFile(resolved, 'utf8');
|
|
69
|
+
const stat = await fs.stat(resolved);
|
|
70
|
+
return {
|
|
71
|
+
path: filePath,
|
|
72
|
+
size: stat.size,
|
|
73
|
+
updatedAt: stat.mtime.toISOString(),
|
|
74
|
+
content
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
export const backupMemory = async (_params, context) => {
|
|
78
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
79
|
+
const backupRoot = path.join(context.projectRoot, 'backups', 'memory', stamp);
|
|
80
|
+
const manifest = {
|
|
81
|
+
createdAt: new Date().toISOString(),
|
|
82
|
+
files: []
|
|
83
|
+
};
|
|
84
|
+
await ensureDir(backupRoot);
|
|
85
|
+
if (await copyIfExists(path.join(context.openclawRoot, 'openclaw.json'), path.join(backupRoot, 'openclaw.json'))) {
|
|
86
|
+
manifest.files.push('openclaw.json');
|
|
87
|
+
}
|
|
88
|
+
for (const fileName of WORKSPACE_ROOT_FILES) {
|
|
89
|
+
const source = path.join(context.openclawRoot, 'workspace', fileName);
|
|
90
|
+
const dest = path.join(backupRoot, 'workspace', fileName);
|
|
91
|
+
if (await copyIfExists(source, dest)) {
|
|
92
|
+
manifest.files.push(path.relative(backupRoot, dest).replaceAll('\\', '/'));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const credentialsDir = path.join(context.openclawRoot, 'credentials');
|
|
96
|
+
if (await pathExists(credentialsDir)) {
|
|
97
|
+
const copiedCredentialFiles = await copyDirectoryTree(credentialsDir, path.join(backupRoot, 'credentials'));
|
|
98
|
+
manifest.files.push(...copiedCredentialFiles.map((file) => `credentials/${file}`));
|
|
99
|
+
}
|
|
100
|
+
await fs.writeFile(path.join(backupRoot, 'manifest.json'), JSON.stringify(manifest, null, 2), 'utf8');
|
|
101
|
+
return {
|
|
102
|
+
backupId: stamp,
|
|
103
|
+
backupPath: backupRoot,
|
|
104
|
+
files: manifest.files
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
export const exportMemoryZip = async (params, context) => {
|
|
108
|
+
const objectParams = (params ? expectObject(params) : {});
|
|
109
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
110
|
+
const outputDirInput = objectParams.outputDir ?? path.join(context.projectRoot, 'exports', 'memory');
|
|
111
|
+
const outputDir = path.resolve(outputDirInput);
|
|
112
|
+
const zipFileName = sanitizeZipFileName(objectParams.zipFileName ?? `memory-backup-${stamp}.zip`);
|
|
113
|
+
await ensureDir(outputDir);
|
|
114
|
+
const snapshot = await backupMemory(undefined, context);
|
|
115
|
+
const snapshotRoot = expectString(snapshot.backupPath, 'backupPath');
|
|
116
|
+
const zipPath = path.join(outputDir, zipFileName);
|
|
117
|
+
await zipDirectory(snapshotRoot, zipPath);
|
|
118
|
+
const stat = await fs.stat(zipPath);
|
|
119
|
+
return {
|
|
120
|
+
ok: true,
|
|
121
|
+
zipPath,
|
|
122
|
+
size: stat.size,
|
|
123
|
+
snapshot
|
|
124
|
+
};
|
|
125
|
+
};
|
|
126
|
+
export const importMemoryZip = async (params, context) => {
|
|
127
|
+
const objectParams = expectObject(params);
|
|
128
|
+
const zipPath = path.resolve(expectString(objectParams.zipPath, 'zipPath'));
|
|
129
|
+
const createBackup = objectParams.createBackup !== false;
|
|
130
|
+
const includePaths = Array.isArray(objectParams.includePaths)
|
|
131
|
+
? objectParams.includePaths.filter((item) => typeof item === 'string' && item.trim().length > 0)
|
|
132
|
+
: undefined;
|
|
133
|
+
if (!(await pathExists(zipPath))) {
|
|
134
|
+
throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, 'Zip file does not exist', { zipPath });
|
|
135
|
+
}
|
|
136
|
+
let backupResult = null;
|
|
137
|
+
if (createBackup) {
|
|
138
|
+
backupResult = await backupMemory(undefined, context);
|
|
139
|
+
}
|
|
140
|
+
const extractRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'openclaw-ts-memory-import-'));
|
|
141
|
+
try {
|
|
142
|
+
await unzipArchive(zipPath, extractRoot);
|
|
143
|
+
const importRoot = await resolveImportedRoot(extractRoot);
|
|
144
|
+
const restoredFiles = await restoreImportedMemory(importRoot, context.openclawRoot, includePaths);
|
|
145
|
+
return {
|
|
146
|
+
ok: true,
|
|
147
|
+
zipPath,
|
|
148
|
+
restoredFiles,
|
|
149
|
+
backup: backupResult
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
finally {
|
|
153
|
+
await fs.rm(extractRoot, { recursive: true, force: true });
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
export const getMemoryPresignedPost = async (params, context) => {
|
|
157
|
+
const objectParams = expectObject(params);
|
|
158
|
+
const body = isObject(objectParams.body) ? objectParams.body : null;
|
|
159
|
+
if (!body) {
|
|
160
|
+
throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, 'Missing required parameter: body');
|
|
161
|
+
}
|
|
162
|
+
const endpointConfig = await resolveApiCoreBotEndpoint(context.openclawRoot, objectParams.baseUrl, objectParams.authToken);
|
|
163
|
+
const response = await postJson(`${endpointConfig.baseUrl}/api-core-bot/front/s3/get-presigned-post`, body, endpointConfig.authToken);
|
|
164
|
+
return {
|
|
165
|
+
ok: true,
|
|
166
|
+
endpoint: '/api-core-bot/front/s3/get-presigned-post',
|
|
167
|
+
data: response
|
|
168
|
+
};
|
|
169
|
+
};
|
|
170
|
+
export const createMemoryBackupRecord = async (params, context) => {
|
|
171
|
+
const objectParams = expectObject(params);
|
|
172
|
+
const body = isObject(objectParams.body) ? objectParams.body : null;
|
|
173
|
+
if (!body) {
|
|
174
|
+
throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, 'Missing required parameter: body');
|
|
175
|
+
}
|
|
176
|
+
const endpointConfig = await resolveApiCoreBotEndpoint(context.openclawRoot, objectParams.baseUrl, objectParams.authToken);
|
|
177
|
+
const response = await postJson(`${endpointConfig.baseUrl}/api-core-bot/front/file/backup/create`, body, endpointConfig.authToken);
|
|
178
|
+
return {
|
|
179
|
+
ok: true,
|
|
180
|
+
endpoint: '/api-core-bot/front/file/backup/create',
|
|
181
|
+
data: response
|
|
182
|
+
};
|
|
183
|
+
};
|
|
184
|
+
function expectObject(value) {
|
|
185
|
+
if (!value || Array.isArray(value) || typeof value !== 'object') {
|
|
186
|
+
throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, 'Params must be an object');
|
|
187
|
+
}
|
|
188
|
+
return value;
|
|
189
|
+
}
|
|
190
|
+
function expectString(value, fieldName) {
|
|
191
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
192
|
+
throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, `Field '${fieldName}' must be a non-empty string`);
|
|
193
|
+
}
|
|
194
|
+
return value;
|
|
195
|
+
}
|
|
196
|
+
function resolveMemoryPath(openclawRoot, relativePath) {
|
|
197
|
+
const workspaceDir = path.join(openclawRoot, 'workspace');
|
|
198
|
+
const allowedRoots = [
|
|
199
|
+
workspaceDir,
|
|
200
|
+
path.join(openclawRoot, 'credentials'),
|
|
201
|
+
openclawRoot
|
|
202
|
+
];
|
|
203
|
+
const candidate = path.join(openclawRoot, relativePath);
|
|
204
|
+
for (const allowedRoot of allowedRoots) {
|
|
205
|
+
try {
|
|
206
|
+
return ensureInside(allowedRoot, candidate);
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, 'Only workspace memory files are readable', { path: relativePath });
|
|
213
|
+
}
|
|
214
|
+
function sanitizeZipFileName(fileName) {
|
|
215
|
+
return fileName.endsWith('.zip') ? fileName : `${fileName}.zip`;
|
|
216
|
+
}
|
|
217
|
+
async function resolveApiCoreBotEndpoint(openclawRoot, overrideBaseUrl, overrideAuthToken) {
|
|
218
|
+
const config = await readJsonFile(path.join(openclawRoot, 'openclaw.json'));
|
|
219
|
+
const pluginConfig = config.plugins?.entries?.['rol-websocket-channel']?.config?.apiCoreBot ?? {};
|
|
220
|
+
const baseUrl = (overrideBaseUrl && overrideBaseUrl.trim()) || pluginConfig.baseUrl;
|
|
221
|
+
if (!baseUrl || !baseUrl.trim()) {
|
|
222
|
+
throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, 'apiCoreBot.baseUrl is not configured');
|
|
223
|
+
}
|
|
224
|
+
const authToken = (overrideAuthToken && overrideAuthToken.trim()) || pluginConfig.authToken;
|
|
225
|
+
return {
|
|
226
|
+
baseUrl: baseUrl.replace(/\/+$/, ''),
|
|
227
|
+
authToken
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
async function postJson(url, body, authToken) {
|
|
231
|
+
const headers = {
|
|
232
|
+
'Content-Type': 'application/json'
|
|
233
|
+
};
|
|
234
|
+
if (authToken && authToken.trim()) {
|
|
235
|
+
headers.Authorization = `Bearer ${authToken.trim()}`;
|
|
236
|
+
}
|
|
237
|
+
const response = await fetch(url, {
|
|
238
|
+
method: 'POST',
|
|
239
|
+
headers,
|
|
240
|
+
body: JSON.stringify(body)
|
|
241
|
+
});
|
|
242
|
+
const payload = await response.json().catch(async () => await response.text());
|
|
243
|
+
if (!response.ok) {
|
|
244
|
+
throw new JsonRpcException(JSON_RPC_ERRORS.internalError, `Request failed: ${response.status}`, {
|
|
245
|
+
url,
|
|
246
|
+
status: response.status,
|
|
247
|
+
payload
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
return payload;
|
|
251
|
+
}
|
|
252
|
+
async function zipDirectory(sourceDir, zipPath) {
|
|
253
|
+
await fs.rm(zipPath, { force: true });
|
|
254
|
+
if (process.platform === 'win32') {
|
|
255
|
+
await execFileAsync('powershell.exe', [
|
|
256
|
+
'-NoProfile',
|
|
257
|
+
'-Command',
|
|
258
|
+
`Compress-Archive -Path '${sourceDir}\\*' -DestinationPath '${zipPath}' -Force`
|
|
259
|
+
]);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
await execFileAsync('zip', ['-r', zipPath, '.'], {
|
|
263
|
+
cwd: sourceDir
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
async function unzipArchive(zipPath, extractRoot) {
|
|
267
|
+
if (process.platform === 'win32') {
|
|
268
|
+
await execFileAsync('powershell.exe', [
|
|
269
|
+
'-NoProfile',
|
|
270
|
+
'-Command',
|
|
271
|
+
`Expand-Archive -Path '${zipPath}' -DestinationPath '${extractRoot}' -Force`
|
|
272
|
+
]);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
await execFileAsync('unzip', ['-o', zipPath, '-d', extractRoot]);
|
|
276
|
+
}
|
|
277
|
+
async function resolveImportedRoot(extractRoot) {
|
|
278
|
+
const directWorkspace = path.join(extractRoot, 'workspace');
|
|
279
|
+
const directConfig = path.join(extractRoot, 'openclaw.json');
|
|
280
|
+
if ((await pathExists(directWorkspace)) || (await pathExists(directConfig))) {
|
|
281
|
+
return extractRoot;
|
|
282
|
+
}
|
|
283
|
+
const entries = await fs.readdir(extractRoot, { withFileTypes: true });
|
|
284
|
+
for (const entry of entries) {
|
|
285
|
+
if (!entry.isDirectory()) {
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
const candidate = path.join(extractRoot, entry.name);
|
|
289
|
+
if ((await pathExists(path.join(candidate, 'workspace'))) || (await pathExists(path.join(candidate, 'openclaw.json')))) {
|
|
290
|
+
return candidate;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, 'Imported zip does not contain a valid memory snapshot');
|
|
294
|
+
}
|
|
295
|
+
async function restoreImportedMemory(importRoot, openclawRoot, includePaths) {
|
|
296
|
+
const restored = [];
|
|
297
|
+
const workspaceRoot = path.join(openclawRoot, 'workspace');
|
|
298
|
+
const importedCredentialsDir = path.join(importRoot, 'credentials');
|
|
299
|
+
if (shouldRestorePath('openclaw.json', includePaths) && await copyIfExists(path.join(importRoot, 'openclaw.json'), path.join(openclawRoot, 'openclaw.json'))) {
|
|
300
|
+
restored.push('openclaw.json');
|
|
301
|
+
}
|
|
302
|
+
for (const fileName of WORKSPACE_ROOT_FILES) {
|
|
303
|
+
const source = path.join(importRoot, 'workspace', fileName);
|
|
304
|
+
const dest = path.join(workspaceRoot, fileName);
|
|
305
|
+
const logicalPath = `workspace/${fileName}`;
|
|
306
|
+
if (shouldRestorePath(logicalPath, includePaths) && await copyIfExists(source, dest)) {
|
|
307
|
+
restored.push(`workspace/${fileName}`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
if ((shouldRestorePath('credentials/', includePaths) || hasNestedInclude('credentials/', includePaths)) && await pathExists(importedCredentialsDir)) {
|
|
311
|
+
const restoredCredentialFiles = await copyDirectoryTree(importedCredentialsDir, path.join(openclawRoot, 'credentials'), '', includePaths);
|
|
312
|
+
restored.push(...restoredCredentialFiles.map((file) => `credentials/${file}`));
|
|
313
|
+
}
|
|
314
|
+
return restored;
|
|
315
|
+
}
|
|
316
|
+
async function copyDirectoryTree(sourceDir, destDir, relativePrefix = '', includePaths) {
|
|
317
|
+
await ensureDir(destDir);
|
|
318
|
+
const copied = [];
|
|
319
|
+
const entries = await fs.readdir(sourceDir, { withFileTypes: true });
|
|
320
|
+
for (const entry of entries) {
|
|
321
|
+
const source = path.join(sourceDir, entry.name);
|
|
322
|
+
const dest = path.join(destDir, entry.name);
|
|
323
|
+
const relativePath = relativePrefix ? `${relativePrefix}/${entry.name}` : entry.name;
|
|
324
|
+
const logicalPath = `credentials/${relativePath.replaceAll('\\', '/')}`;
|
|
325
|
+
if (entry.isDirectory()) {
|
|
326
|
+
if (!hasNestedInclude(`${logicalPath}/`, includePaths) && !shouldRestorePath(`${logicalPath}/`, includePaths)) {
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
const nested = await copyDirectoryTree(source, dest, relativePath, includePaths);
|
|
330
|
+
copied.push(...nested);
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
if (!entry.isFile()) {
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
if (!shouldRestorePath(logicalPath, includePaths) && !shouldRestorePath('credentials/', includePaths)) {
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
await copyIfExists(source, dest);
|
|
340
|
+
copied.push(relativePath.replaceAll('\\', '/'));
|
|
341
|
+
}
|
|
342
|
+
return copied;
|
|
343
|
+
}
|
|
344
|
+
function shouldRestorePath(targetPath, includePaths) {
|
|
345
|
+
if (!includePaths || includePaths.length === 0) {
|
|
346
|
+
return true;
|
|
347
|
+
}
|
|
348
|
+
const normalizedTarget = normalizeLogicalPath(targetPath);
|
|
349
|
+
return includePaths.some((value) => normalizeLogicalPath(value) === normalizedTarget);
|
|
350
|
+
}
|
|
351
|
+
function hasNestedInclude(prefix, includePaths) {
|
|
352
|
+
if (!includePaths || includePaths.length === 0) {
|
|
353
|
+
return true;
|
|
354
|
+
}
|
|
355
|
+
const normalizedPrefix = normalizeLogicalPath(prefix).replace(/\/?$/, '/');
|
|
356
|
+
return includePaths.some((value) => normalizeLogicalPath(value).startsWith(normalizedPrefix));
|
|
357
|
+
}
|
|
358
|
+
function normalizeLogicalPath(value) {
|
|
359
|
+
return value.replaceAll('\\', '/').replace(/^\/+/, '').replace(/\/+$/, '');
|
|
360
|
+
}
|