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,35 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export async function readJsonFile<T>(filePath: string): Promise<T> {
|
|
5
|
+
const raw = await fs.readFile(filePath, 'utf8');
|
|
6
|
+
return JSON.parse(raw) as T;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function writeJsonFile(filePath: string, data: any): Promise<void> {
|
|
10
|
+
const json = JSON.stringify(data, null, 2);
|
|
11
|
+
await fs.writeFile(filePath, json, 'utf8');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function pathExists(targetPath: string): Promise<boolean> {
|
|
15
|
+
try {
|
|
16
|
+
await fs.access(targetPath);
|
|
17
|
+
return true;
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function ensureDir(targetDir: string): Promise<void> {
|
|
24
|
+
await fs.mkdir(targetDir, { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function copyIfExists(sourcePath: string, destPath: string): Promise<boolean> {
|
|
28
|
+
if (!(await pathExists(sourcePath))) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
await ensureDir(path.dirname(destPath));
|
|
33
|
+
await fs.copyFile(sourcePath, destPath);
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
|
|
4
|
+
import { JsonRpcException, JSON_RPC_ERRORS } from '../jsonrpc.ts';
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
|
|
9
|
+
export function getProjectRoot(): string {
|
|
10
|
+
// __dirname 是 /path/to/rol-websocket-channel/src/admin/lib
|
|
11
|
+
// 需要返回 /path/to/rol-websocket-channel
|
|
12
|
+
const root = path.resolve(__dirname, '..', '..', '..');
|
|
13
|
+
console.log('[paths] __dirname:', __dirname);
|
|
14
|
+
console.log('[paths] getProjectRoot:', root);
|
|
15
|
+
return root;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function getOpenClawRoot(): string {
|
|
19
|
+
if (process.env.OPENCLAW_HOME) {
|
|
20
|
+
console.log('[paths] getOpenClawRoot from env:', process.env.OPENCLAW_HOME);
|
|
21
|
+
return path.resolve(process.env.OPENCLAW_HOME);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// 插件在 ~/.openclaw/extensions/rol-websocket-channel
|
|
25
|
+
// 需要返回 ~/.openclaw
|
|
26
|
+
const projectRoot = getProjectRoot();
|
|
27
|
+
const parentDir = path.dirname(projectRoot);
|
|
28
|
+
const parentName = path.basename(parentDir);
|
|
29
|
+
|
|
30
|
+
console.log('[paths] projectRoot:', projectRoot);
|
|
31
|
+
console.log('[paths] parentDir:', parentDir);
|
|
32
|
+
console.log('[paths] parentName:', parentName);
|
|
33
|
+
|
|
34
|
+
if (parentName === 'extensions') {
|
|
35
|
+
// parentDir 是 ~/.openclaw/extensions
|
|
36
|
+
// 返回 ~/.openclaw
|
|
37
|
+
const openclawRoot = path.dirname(parentDir);
|
|
38
|
+
console.log('[paths] getOpenClawRoot (extensions):', openclawRoot);
|
|
39
|
+
return openclawRoot;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 兜底:假设插件在 openclaw 根目录的子目录
|
|
43
|
+
const fallbackRoot = path.resolve(projectRoot, '..');
|
|
44
|
+
console.log('[paths] getOpenClawRoot (fallback):', fallbackRoot);
|
|
45
|
+
return fallbackRoot;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function ensureInside(parentDir: string, targetPath: string): string {
|
|
49
|
+
const parent = path.resolve(parentDir);
|
|
50
|
+
const target = path.resolve(targetPath);
|
|
51
|
+
|
|
52
|
+
if (target !== parent && !target.startsWith(`${parent}${path.sep}`)) {
|
|
53
|
+
throw new JsonRpcException(
|
|
54
|
+
JSON_RPC_ERRORS.invalidParams,
|
|
55
|
+
'Path escapes allowed directory',
|
|
56
|
+
{ parent, target }
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return target;
|
|
61
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
import { readJsonFile } from '../lib/fs.ts';
|
|
4
|
+
import type { JsonValue, MethodHandler } from '../types.ts';
|
|
5
|
+
|
|
6
|
+
interface OpenClawConfig {
|
|
7
|
+
agents?: {
|
|
8
|
+
defaults?: Record<string, JsonValue>;
|
|
9
|
+
list?: JsonValue[];
|
|
10
|
+
};
|
|
11
|
+
plugins?: {
|
|
12
|
+
allow?: string[];
|
|
13
|
+
entries?: Record<string, JsonValue>;
|
|
14
|
+
};
|
|
15
|
+
tools?: Record<string, JsonValue>;
|
|
16
|
+
gateway?: Record<string, JsonValue>;
|
|
17
|
+
session?: Record<string, JsonValue>;
|
|
18
|
+
models?: Record<string, JsonValue>;
|
|
19
|
+
skills?: Record<string, JsonValue>;
|
|
20
|
+
diagnostics?: Record<string, JsonValue>;
|
|
21
|
+
[key: string]: JsonValue | undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const getAgents: MethodHandler = async (_params, context): Promise<JsonValue> => {
|
|
25
|
+
const configPath = path.join(context.openclawRoot, 'openclaw.json');
|
|
26
|
+
const config = await readJsonFile<OpenClawConfig>(configPath);
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
sourceConfigFile: configPath,
|
|
30
|
+
defaultAgentConfig: config.agents?.defaults ?? {},
|
|
31
|
+
namedAgents: config.agents?.list ?? [],
|
|
32
|
+
activeToolProfile: (config.tools?.profile as JsonValue | undefined) ?? null
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const getConfig: MethodHandler = async (params, context): Promise<JsonValue> => {
|
|
37
|
+
const configPath = path.join(context.openclawRoot, 'openclaw.json');
|
|
38
|
+
const config = await readJsonFile<OpenClawConfig>(configPath);
|
|
39
|
+
const objectParams = isObject(params) ? params : {};
|
|
40
|
+
const section = typeof objectParams.section === 'string' ? objectParams.section : null;
|
|
41
|
+
|
|
42
|
+
if (!section) {
|
|
43
|
+
return {
|
|
44
|
+
sourceConfigFile: configPath,
|
|
45
|
+
config: redactSecrets(config as unknown as JsonValue)
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const value = config[section];
|
|
50
|
+
return {
|
|
51
|
+
sourceConfigFile: configPath,
|
|
52
|
+
section,
|
|
53
|
+
sectionValue: value === undefined ? null : redactSecrets(value)
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
function isObject(value: JsonValue | undefined): value is Record<string, JsonValue> {
|
|
58
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function redactSecrets(value: JsonValue): JsonValue {
|
|
62
|
+
if (Array.isArray(value)) {
|
|
63
|
+
return value.map(redactSecrets);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!value || typeof value !== 'object') {
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const result: Record<string, JsonValue> = {};
|
|
71
|
+
|
|
72
|
+
for (const [key, nestedValue] of Object.entries(value)) {
|
|
73
|
+
if (isSecretKey(key) && typeof nestedValue === 'string') {
|
|
74
|
+
result[key] = redactString(nestedValue);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
result[key] = redactSecrets(nestedValue);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function isSecretKey(key: string): boolean {
|
|
85
|
+
const normalized = key.toLowerCase();
|
|
86
|
+
return normalized.includes('apikey') || normalized.includes('token') || normalized.includes('secret');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function redactString(value: string): string {
|
|
90
|
+
if (value.length <= 8) {
|
|
91
|
+
return '********';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return `${value.slice(0, 4)}***${value.slice(-4)}`;
|
|
95
|
+
}
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { pathExists, readJsonFile, writeJsonFile } from '../lib/fs.ts';
|
|
5
|
+
import { JsonRpcException, JSON_RPC_ERRORS } from '../jsonrpc.ts';
|
|
6
|
+
import type { JsonValue, MethodHandler } from '../types.ts';
|
|
7
|
+
|
|
8
|
+
interface OpenClawConfig {
|
|
9
|
+
agents?: {
|
|
10
|
+
defaults?: Record<string, any>;
|
|
11
|
+
list?: NamedAgentRecord[];
|
|
12
|
+
};
|
|
13
|
+
[key: string]: any;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface NamedAgentRecord {
|
|
17
|
+
id: string;
|
|
18
|
+
name?: string;
|
|
19
|
+
workspace?: string;
|
|
20
|
+
agentDir?: string;
|
|
21
|
+
model?: Record<string, any>;
|
|
22
|
+
skills?: string[];
|
|
23
|
+
tools?: Record<string, any>;
|
|
24
|
+
behavior?: JsonValue;
|
|
25
|
+
[key: string]: any;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const ALLOWED_AGENT_UPDATES: Record<string, boolean> = {
|
|
29
|
+
'name': true,
|
|
30
|
+
'workspace': true,
|
|
31
|
+
'agentDir': true,
|
|
32
|
+
'model.primary': true,
|
|
33
|
+
'model.provider': true,
|
|
34
|
+
'skills': true,
|
|
35
|
+
'tools.profile': true,
|
|
36
|
+
'behavior': true,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const listAgents: MethodHandler = async (_params, context): Promise<JsonValue> => {
|
|
40
|
+
const configPath = path.join(context.openclawRoot, 'openclaw.json');
|
|
41
|
+
const config = await loadConfig(context.openclawRoot);
|
|
42
|
+
const items = normalizeAgentList(config);
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
sourceConfigFile: configPath,
|
|
46
|
+
count: items.length,
|
|
47
|
+
items
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const createAgent: MethodHandler = async (params, context): Promise<JsonValue> => {
|
|
52
|
+
const objectParams = expectObject(params);
|
|
53
|
+
const rawAgentId = typeof objectParams.agentId === 'string'
|
|
54
|
+
? objectParams.agentId
|
|
55
|
+
: typeof objectParams.id === 'string'
|
|
56
|
+
? objectParams.id
|
|
57
|
+
: undefined;
|
|
58
|
+
const agentId = sanitizeAgentId(expectString(rawAgentId, 'agentId'));
|
|
59
|
+
const configPath = path.join(context.openclawRoot, 'openclaw.json');
|
|
60
|
+
const config = await loadConfig(context.openclawRoot);
|
|
61
|
+
const agents = ensureAgentList(config);
|
|
62
|
+
|
|
63
|
+
if (findNamedAgent(agents, agentId)) {
|
|
64
|
+
throw new JsonRpcException(
|
|
65
|
+
JSON_RPC_ERRORS.invalidParams,
|
|
66
|
+
`Agent already exists: ${agentId}`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const workspace = typeof objectParams.workspace === 'string' && objectParams.workspace.trim().length > 0
|
|
71
|
+
? objectParams.workspace.trim()
|
|
72
|
+
: path.join(context.openclawRoot, 'workspace', agentId);
|
|
73
|
+
const agentDir = typeof objectParams.agentDir === 'string' && objectParams.agentDir.trim().length > 0
|
|
74
|
+
? objectParams.agentDir.trim()
|
|
75
|
+
: path.join(context.openclawRoot, 'agents', agentId, 'agent');
|
|
76
|
+
|
|
77
|
+
const created: NamedAgentRecord = {
|
|
78
|
+
id: agentId,
|
|
79
|
+
name: typeof objectParams.name === 'string' && objectParams.name.trim().length > 0
|
|
80
|
+
? objectParams.name.trim()
|
|
81
|
+
: agentId,
|
|
82
|
+
workspace,
|
|
83
|
+
agentDir
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
if (typeof objectParams.modelPrimary === 'string' && objectParams.modelPrimary.trim().length > 0) {
|
|
87
|
+
created.model = { ...(created.model ?? {}), primary: objectParams.modelPrimary.trim() };
|
|
88
|
+
}
|
|
89
|
+
if (typeof objectParams.modelProvider === 'string' && objectParams.modelProvider.trim().length > 0) {
|
|
90
|
+
created.model = { ...(created.model ?? {}), provider: objectParams.modelProvider.trim() };
|
|
91
|
+
}
|
|
92
|
+
if (Array.isArray(objectParams.skills)) {
|
|
93
|
+
created.skills = objectParams.skills.filter((item): item is string => typeof item === 'string');
|
|
94
|
+
}
|
|
95
|
+
if (typeof objectParams.toolsProfile === 'string' && objectParams.toolsProfile.trim().length > 0) {
|
|
96
|
+
created.tools = { ...(created.tools ?? {}), profile: objectParams.toolsProfile.trim() };
|
|
97
|
+
}
|
|
98
|
+
if (isObject(objectParams.behavior)) {
|
|
99
|
+
created.behavior = objectParams.behavior;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
agents.push(created);
|
|
103
|
+
|
|
104
|
+
await fs.mkdir(workspace, { recursive: true });
|
|
105
|
+
await fs.mkdir(agentDir, { recursive: true });
|
|
106
|
+
await writeJsonFile(configPath, config);
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
success: true,
|
|
110
|
+
configFile: configPath,
|
|
111
|
+
created
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export const deleteAgent: MethodHandler = async (params, context): Promise<JsonValue> => {
|
|
116
|
+
const objectParams = expectObject(params);
|
|
117
|
+
const rawAgentId = typeof objectParams.agentId === 'string'
|
|
118
|
+
? objectParams.agentId
|
|
119
|
+
: typeof objectParams.id === 'string'
|
|
120
|
+
? objectParams.id
|
|
121
|
+
: undefined;
|
|
122
|
+
const agentId = sanitizeAgentId(expectString(rawAgentId, 'agentId'));
|
|
123
|
+
const purgeFiles = objectParams.purgeFiles === true;
|
|
124
|
+
|
|
125
|
+
const configPath = path.join(context.openclawRoot, 'openclaw.json');
|
|
126
|
+
const config = await loadConfig(context.openclawRoot);
|
|
127
|
+
const agents = ensureAgentList(config);
|
|
128
|
+
const index = agents.findIndex((agent) => isMatchingAgent(agent, agentId));
|
|
129
|
+
|
|
130
|
+
if (index === -1) {
|
|
131
|
+
throw new JsonRpcException(
|
|
132
|
+
JSON_RPC_ERRORS.invalidParams,
|
|
133
|
+
`Agent not found: ${agentId}`
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const [removed] = agents.splice(index, 1);
|
|
138
|
+
await writeJsonFile(configPath, config);
|
|
139
|
+
|
|
140
|
+
const removedPaths: string[] = [];
|
|
141
|
+
if (purgeFiles) {
|
|
142
|
+
const workspaceRoot = path.normalize(path.join(context.openclawRoot, 'workspace'));
|
|
143
|
+
const agentsRoot = path.normalize(path.join(context.openclawRoot, 'agents'));
|
|
144
|
+
const purgeCandidates = [
|
|
145
|
+
typeof removed.workspace === 'string' ? removed.workspace : null,
|
|
146
|
+
typeof removed.agentDir === 'string' ? path.dirname(removed.agentDir) : null
|
|
147
|
+
].filter((value): value is string => Boolean(value));
|
|
148
|
+
|
|
149
|
+
for (const candidate of purgeCandidates) {
|
|
150
|
+
const normalized = path.normalize(candidate);
|
|
151
|
+
const insideWorkspace = normalized === workspaceRoot || normalized.startsWith(`${workspaceRoot}${path.sep}`);
|
|
152
|
+
const insideAgents = normalized === agentsRoot || normalized.startsWith(`${agentsRoot}${path.sep}`);
|
|
153
|
+
if (!insideWorkspace && !insideAgents) {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (await pathExists(candidate)) {
|
|
157
|
+
await fs.rm(candidate, { recursive: true, force: true });
|
|
158
|
+
removedPaths.push(candidate);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
success: true,
|
|
165
|
+
configFile: configPath,
|
|
166
|
+
removed,
|
|
167
|
+
purgeFiles,
|
|
168
|
+
removedPaths
|
|
169
|
+
};
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
export const updateAgent: MethodHandler = async (params, context): Promise<JsonValue> => {
|
|
173
|
+
const objectParams = isObject(params) ? params : {};
|
|
174
|
+
const updates = isObject(objectParams.updates) ? objectParams.updates : null;
|
|
175
|
+
const rawAgentId = typeof objectParams.agentId === 'string'
|
|
176
|
+
? objectParams.agentId
|
|
177
|
+
: typeof objectParams.agentName === 'string'
|
|
178
|
+
? objectParams.agentName
|
|
179
|
+
: typeof objectParams.id === 'string'
|
|
180
|
+
? objectParams.id
|
|
181
|
+
: 'defaults';
|
|
182
|
+
const agentId = typeof rawAgentId === 'string' && rawAgentId !== 'defaults'
|
|
183
|
+
? sanitizeAgentId(rawAgentId)
|
|
184
|
+
: rawAgentId;
|
|
185
|
+
|
|
186
|
+
if (!updates) {
|
|
187
|
+
throw new JsonRpcException(
|
|
188
|
+
JSON_RPC_ERRORS.invalidParams,
|
|
189
|
+
'Missing required parameter: updates (object with field paths and values)'
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const configPath = path.join(context.openclawRoot, 'openclaw.json');
|
|
194
|
+
const config = await loadConfig(context.openclawRoot);
|
|
195
|
+
const target = resolveAgentTarget(config, agentId);
|
|
196
|
+
const changes: string[] = [];
|
|
197
|
+
|
|
198
|
+
for (const [fieldPath, value] of Object.entries(updates)) {
|
|
199
|
+
if (!ALLOWED_AGENT_UPDATES[fieldPath]) {
|
|
200
|
+
throw new JsonRpcException(
|
|
201
|
+
JSON_RPC_ERRORS.invalidParams,
|
|
202
|
+
`Field not allowed for update: ${fieldPath}. Allowed fields: ${Object.keys(ALLOWED_AGENT_UPDATES).join(', ')}`
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
setNestedValue(target, fieldPath, value);
|
|
207
|
+
changes.push(`Updated ${fieldPath} to: ${JSON.stringify(value)}`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
await writeJsonFile(configPath, config);
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
success: true,
|
|
214
|
+
configFile: configPath,
|
|
215
|
+
agentId,
|
|
216
|
+
changes,
|
|
217
|
+
updatedConfig: target
|
|
218
|
+
};
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
function setNestedValue(obj: any, pathExpression: string, value: any): void {
|
|
222
|
+
const keys = pathExpression.split('.');
|
|
223
|
+
let current = obj;
|
|
224
|
+
|
|
225
|
+
for (let index = 0; index < keys.length - 1; index += 1) {
|
|
226
|
+
const key = keys[index];
|
|
227
|
+
if (!current[key] || typeof current[key] !== 'object') {
|
|
228
|
+
current[key] = {};
|
|
229
|
+
}
|
|
230
|
+
current = current[key];
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
current[keys[keys.length - 1]] = value;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function isObject(value: JsonValue | undefined): value is Record<string, JsonValue> {
|
|
237
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function expectObject(value: JsonValue | undefined): Record<string, JsonValue> {
|
|
241
|
+
if (!value || Array.isArray(value) || typeof value !== 'object') {
|
|
242
|
+
throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, 'Params must be an object');
|
|
243
|
+
}
|
|
244
|
+
return value as Record<string, JsonValue>;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function expectString(value: JsonValue | undefined, fieldName: string): string {
|
|
248
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
249
|
+
throw new JsonRpcException(
|
|
250
|
+
JSON_RPC_ERRORS.invalidParams,
|
|
251
|
+
`Field '${fieldName}' must be a non-empty string`
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
return value.trim();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function loadConfig(openclawRoot: string): Promise<OpenClawConfig> {
|
|
258
|
+
return await readJsonFile<OpenClawConfig>(path.join(openclawRoot, 'openclaw.json'));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function ensureAgentList(config: OpenClawConfig): NamedAgentRecord[] {
|
|
262
|
+
if (!config.agents) config.agents = {};
|
|
263
|
+
if (!Array.isArray(config.agents.list)) config.agents.list = [];
|
|
264
|
+
return config.agents.list;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function normalizeAgentList(config: OpenClawConfig): NamedAgentRecord[] {
|
|
268
|
+
const defaults: NamedAgentRecord = {
|
|
269
|
+
id: 'defaults',
|
|
270
|
+
name: 'defaults',
|
|
271
|
+
...(config.agents?.defaults ?? {})
|
|
272
|
+
};
|
|
273
|
+
const named = Array.isArray(config.agents?.list) ? config.agents.list : [];
|
|
274
|
+
return [defaults, ...named];
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function resolveAgentTarget(config: OpenClawConfig, agentName: string): Record<string, any> {
|
|
278
|
+
if (agentName === 'defaults') {
|
|
279
|
+
if (!config.agents) config.agents = {};
|
|
280
|
+
if (!config.agents.defaults) config.agents.defaults = {};
|
|
281
|
+
return config.agents.defaults;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const agents = ensureAgentList(config);
|
|
285
|
+
const found = findNamedAgent(agents, agentName);
|
|
286
|
+
if (!found) {
|
|
287
|
+
throw new JsonRpcException(
|
|
288
|
+
JSON_RPC_ERRORS.invalidParams,
|
|
289
|
+
`Agent not found: ${agentName}`
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
return found;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function findNamedAgent(agents: NamedAgentRecord[], agentName: string): NamedAgentRecord | null {
|
|
296
|
+
return agents.find((agent) => isMatchingAgent(agent, agentName)) ?? null;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function isMatchingAgent(agent: NamedAgentRecord, agentName: string): boolean {
|
|
300
|
+
return agent.id === agentName || agent.name === agentName;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function sanitizeAgentId(value: string): string {
|
|
304
|
+
return value
|
|
305
|
+
.trim()
|
|
306
|
+
.replace(/[^a-zA-Z0-9._-]+/g, '-')
|
|
307
|
+
.replace(/-+/g, '-')
|
|
308
|
+
.replace(/^-|-$/g, '')
|
|
309
|
+
.toLowerCase();
|
|
310
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { JsonRpcException, JSON_RPC_ERRORS } from '../jsonrpc.ts';
|
|
2
|
+
import type { MethodHandler } from '../types.ts';
|
|
3
|
+
import { getAgents, getConfig } from './admin.ts';
|
|
4
|
+
import { createAgent, deleteAgent, listAgents, updateAgent } from './agents-extended.ts';
|
|
5
|
+
import {
|
|
6
|
+
backupMemory,
|
|
7
|
+
createMemoryBackupRecord,
|
|
8
|
+
exportMemoryZip,
|
|
9
|
+
getMemoryPresignedPost,
|
|
10
|
+
getMemoryFile,
|
|
11
|
+
importMemoryZip,
|
|
12
|
+
listMemoryFiles
|
|
13
|
+
} from './memory.ts';
|
|
14
|
+
import { getModels } from './models.ts';
|
|
15
|
+
import { updateModels } from './models-extended.ts';
|
|
16
|
+
import { listSessions } from './sessions.ts';
|
|
17
|
+
import { getSession, prepareMessage, attachSkill } from './sessions-extended.ts';
|
|
18
|
+
import {
|
|
19
|
+
installSkillFromClawHub,
|
|
20
|
+
installSkillFromNpm,
|
|
21
|
+
listInstalledSkills,
|
|
22
|
+
searchClawHubSkills,
|
|
23
|
+
updateSkillFromClawHub
|
|
24
|
+
} from './skills.ts';
|
|
25
|
+
import { getInstalledSkill, uninstallSkill } from './skills-extended.ts';
|
|
26
|
+
import { ping, restart, stop, doctorFix, logs } from './system.ts';
|
|
27
|
+
import {
|
|
28
|
+
getUsageBreakdown,
|
|
29
|
+
getUsagePageSummary,
|
|
30
|
+
getUsageSummary,
|
|
31
|
+
getUsageTimeseries
|
|
32
|
+
} from './usage.ts';
|
|
33
|
+
|
|
34
|
+
const methods = new Map<string, MethodHandler>([
|
|
35
|
+
// System
|
|
36
|
+
['system.ping', ping],
|
|
37
|
+
['system.restart', restart],
|
|
38
|
+
['system.stop', stop],
|
|
39
|
+
['system.doctorFix', doctorFix],
|
|
40
|
+
['system.logs', logs],
|
|
41
|
+
|
|
42
|
+
// Agents
|
|
43
|
+
['agents.get', getAgents],
|
|
44
|
+
['agents.list', listAgents],
|
|
45
|
+
['agents.create', createAgent],
|
|
46
|
+
['agents.delete', deleteAgent],
|
|
47
|
+
['agents.update', updateAgent],
|
|
48
|
+
|
|
49
|
+
// Config
|
|
50
|
+
['config.get', getConfig],
|
|
51
|
+
|
|
52
|
+
// Sessions
|
|
53
|
+
['sessions.list', listSessions],
|
|
54
|
+
['sessions.get', getSession],
|
|
55
|
+
['sessions.prepareMessage', prepareMessage],
|
|
56
|
+
['sessions.attachSkill', attachSkill],
|
|
57
|
+
|
|
58
|
+
// Models
|
|
59
|
+
['models.get', getModels],
|
|
60
|
+
['models.update', updateModels],
|
|
61
|
+
|
|
62
|
+
// Usage
|
|
63
|
+
['usage.summary', getUsageSummary],
|
|
64
|
+
['adminBridge.usagePageSummary', getUsagePageSummary],
|
|
65
|
+
['adminBridge.usageTimeseries', getUsageTimeseries],
|
|
66
|
+
['adminBridge.usageBreakdown', getUsageBreakdown],
|
|
67
|
+
['admin.usagePageSummary', getUsagePageSummary],
|
|
68
|
+
['admin.usageTimeseries', getUsageTimeseries],
|
|
69
|
+
['admin.usageBreakdown', getUsageBreakdown],
|
|
70
|
+
|
|
71
|
+
// Skills
|
|
72
|
+
['skills.listInstalled', listInstalledSkills],
|
|
73
|
+
['skills.installFromNpm', installSkillFromNpm],
|
|
74
|
+
['skills.searchClawHub', searchClawHubSkills],
|
|
75
|
+
['skills.installFromClawHub', installSkillFromClawHub],
|
|
76
|
+
['skills.updateFromClawHub', updateSkillFromClawHub],
|
|
77
|
+
['skills.getInstalled', getInstalledSkill],
|
|
78
|
+
['skills.uninstall', uninstallSkill],
|
|
79
|
+
|
|
80
|
+
// Memory
|
|
81
|
+
['memory.listFiles', listMemoryFiles],
|
|
82
|
+
['memory.getFile', getMemoryFile],
|
|
83
|
+
['memory.backup', backupMemory],
|
|
84
|
+
['memory.exportZip', exportMemoryZip],
|
|
85
|
+
['memory.getPresignedPost', getMemoryPresignedPost],
|
|
86
|
+
['memory.createBackupRecord', createMemoryBackupRecord],
|
|
87
|
+
['memory.importZip', importMemoryZip]
|
|
88
|
+
]);
|
|
89
|
+
|
|
90
|
+
export function getMethod(methodName: string): MethodHandler {
|
|
91
|
+
const handler = methods.get(methodName);
|
|
92
|
+
if (!handler) {
|
|
93
|
+
throw new JsonRpcException(JSON_RPC_ERRORS.methodNotFound, 'Method not found', {
|
|
94
|
+
method: methodName
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return handler;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function listMethods(): string[] {
|
|
102
|
+
return [...methods.keys()];
|
|
103
|
+
}
|