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.
Files changed (58) hide show
  1. package/MQTT-API.md +967 -0
  2. package/dist/index.js +430 -0
  3. package/dist/message-handler.js +327 -0
  4. package/dist/src/admin/cli.js +43 -0
  5. package/dist/src/admin/jsonrpc.js +60 -0
  6. package/dist/src/admin/lib/fs.js +30 -0
  7. package/dist/src/admin/lib/paths.js +46 -0
  8. package/dist/src/admin/methods/admin.js +60 -0
  9. package/dist/src/admin/methods/agents-extended.js +235 -0
  10. package/dist/src/admin/methods/index.js +69 -0
  11. package/dist/src/admin/methods/memory.js +360 -0
  12. package/dist/src/admin/methods/models-extended.js +107 -0
  13. package/dist/src/admin/methods/models.js +39 -0
  14. package/dist/src/admin/methods/sessions-extended.js +207 -0
  15. package/dist/src/admin/methods/sessions.js +64 -0
  16. package/dist/src/admin/methods/skills-extended.js +157 -0
  17. package/dist/src/admin/methods/skills-toggle.js +182 -0
  18. package/dist/src/admin/methods/skills.js +384 -0
  19. package/dist/src/admin/methods/system.js +178 -0
  20. package/dist/src/admin/methods/usage.js +1170 -0
  21. package/dist/src/admin/types.js +1 -0
  22. package/dist/src/mqtt/connection-manager.js +155 -0
  23. package/dist/src/mqtt/index.js +5 -0
  24. package/dist/src/mqtt/mqtt-client.js +86 -0
  25. package/dist/src/mqtt/types.js +2 -0
  26. package/dist/src/shared/context.js +24 -0
  27. package/dist/src/shared/wrapper.js +23 -0
  28. package/index.ts +514 -0
  29. package/message-handler.ts +415 -0
  30. package/openclaw.plugin.json +84 -0
  31. package/package.json +35 -0
  32. package/readme.md +32 -0
  33. package/src/admin/cli.ts +60 -0
  34. package/src/admin/jsonrpc.ts +88 -0
  35. package/src/admin/lib/fs.ts +35 -0
  36. package/src/admin/lib/paths.ts +61 -0
  37. package/src/admin/methods/admin.ts +95 -0
  38. package/src/admin/methods/agents-extended.ts +310 -0
  39. package/src/admin/methods/index.ts +103 -0
  40. package/src/admin/methods/memory.ts +546 -0
  41. package/src/admin/methods/models-extended.ts +191 -0
  42. package/src/admin/methods/models.ts +103 -0
  43. package/src/admin/methods/sessions-extended.ts +313 -0
  44. package/src/admin/methods/sessions.ts +122 -0
  45. package/src/admin/methods/skills-extended.ts +249 -0
  46. package/src/admin/methods/skills-toggle.ts +235 -0
  47. package/src/admin/methods/skills.ts +651 -0
  48. package/src/admin/methods/system.ts +203 -0
  49. package/src/admin/methods/usage.ts +1491 -0
  50. package/src/admin/types.ts +46 -0
  51. package/src/mqtt/connection-manager.ts +188 -0
  52. package/src/mqtt/index.ts +6 -0
  53. package/src/mqtt/mqtt-client.ts +119 -0
  54. package/src/mqtt/types.ts +36 -0
  55. package/src/shared/context.ts +33 -0
  56. package/src/shared/wrapper.ts +35 -0
  57. package/tsconfig.json +16 -0
  58. package/types/openclaw.d.ts +74 -0
@@ -0,0 +1,191 @@
1
+ import path from 'node:path';
2
+
3
+ import { readJsonFile, writeJsonFile } from '../lib/fs.ts';
4
+ import { JsonRpcException, JSON_RPC_ERRORS } from '../jsonrpc.ts';
5
+ import type { JsonValue, MethodHandler } from '../types.ts';
6
+
7
+ interface OpenClawConfig {
8
+ models?: {
9
+ mode?: string;
10
+ providers?: Record<string, any>;
11
+ };
12
+ agents?: {
13
+ defaults?: {
14
+ model?: {
15
+ primary?: string;
16
+ provider?: string;
17
+ [key: string]: any;
18
+ };
19
+ [key: string]: any;
20
+ };
21
+ [key: string]: any;
22
+ };
23
+ [key: string]: any;
24
+ }
25
+
26
+ // 允许修改的 provider 配置字段白名单
27
+ const ALLOWED_PROVIDER_FIELDS: Record<string, boolean> = {
28
+ 'apiKey': true,
29
+ 'baseUrl': true,
30
+ 'model': true,
31
+ 'temperature': true,
32
+ 'maxTokens': true,
33
+ 'topP': true,
34
+ 'frequencyPenalty': true,
35
+ 'presencePenalty': true,
36
+ };
37
+
38
+ /**
39
+ * 更新模型配置
40
+ * 支持修改 primary model 和 provider 配置
41
+ */
42
+ export const updateModels: MethodHandler = async (
43
+ params,
44
+ context
45
+ ): Promise<JsonValue> => {
46
+ const objectParams = isObject(params) ? params : {};
47
+ const primaryModel = pickString(objectParams.primaryModel);
48
+ const modelProvider = pickString(objectParams.modelProvider);
49
+ const provider = pickString(objectParams.provider);
50
+ const providerConfig = isObject(objectParams.providerConfig) ? objectParams.providerConfig : null;
51
+
52
+ const configPath = path.join(context.openclawRoot, 'openclaw.json');
53
+ const config = await readJsonFile<OpenClawConfig>(configPath);
54
+
55
+ let updated = false;
56
+ const changes: string[] = [];
57
+
58
+ // 更新 primary model
59
+ if (primaryModel) {
60
+ if (!config.agents) config.agents = {};
61
+ if (!config.agents.defaults) config.agents.defaults = {};
62
+ if (!config.agents.defaults.model) config.agents.defaults.model = {};
63
+
64
+ config.agents.defaults.model.primary = primaryModel;
65
+ const inferredProvider = modelProvider ?? inferProviderFromPrimaryModel(primaryModel);
66
+ if (inferredProvider) {
67
+ config.agents.defaults.model.provider = inferredProvider;
68
+ changes.push(`Updated default model provider to: ${inferredProvider}`);
69
+ }
70
+ updated = true;
71
+ changes.push(`Updated primary model to: ${primaryModel}`);
72
+ }
73
+
74
+ if (modelProvider && !primaryModel) {
75
+ if (!config.agents) config.agents = {};
76
+ if (!config.agents.defaults) config.agents.defaults = {};
77
+ if (!config.agents.defaults.model) config.agents.defaults.model = {};
78
+
79
+ config.agents.defaults.model.provider = modelProvider;
80
+ updated = true;
81
+ changes.push(`Updated default model provider to: ${modelProvider}`);
82
+ }
83
+
84
+ // 更新 provider 配置
85
+ if (provider && providerConfig) {
86
+ if (!config.models) config.models = {};
87
+ if (!config.models.providers) config.models.providers = {};
88
+
89
+ // 验证字段白名单
90
+ for (const field of Object.keys(providerConfig)) {
91
+ if (!ALLOWED_PROVIDER_FIELDS[field]) {
92
+ throw new JsonRpcException(
93
+ JSON_RPC_ERRORS.invalidParams,
94
+ `Field not allowed for provider config: ${field}. Allowed fields: ${Object.keys(ALLOWED_PROVIDER_FIELDS).join(', ')}`
95
+ );
96
+ }
97
+ }
98
+
99
+ // 合并配置
100
+ if (!config.models.providers[provider]) {
101
+ config.models.providers[provider] = {};
102
+ }
103
+
104
+ config.models.providers[provider] = {
105
+ ...config.models.providers[provider],
106
+ ...providerConfig
107
+ };
108
+ updated = true;
109
+ changes.push(`Updated provider config for: ${provider}`);
110
+ }
111
+
112
+ if (!updated) {
113
+ throw new JsonRpcException(
114
+ JSON_RPC_ERRORS.invalidParams,
115
+ 'No valid update parameters provided. Use primaryModel, modelProvider, or provider+providerConfig.'
116
+ );
117
+ }
118
+
119
+ // 写回配置文件
120
+ await writeJsonFile(configPath, config);
121
+
122
+ return {
123
+ success: true,
124
+ configFile: configPath,
125
+ changes,
126
+ updatedConfig: {
127
+ primaryModel: config.agents?.defaults?.model?.primary ?? null,
128
+ modelProvider: config.agents?.defaults?.model?.provider ?? null,
129
+ defaultModel: config.agents?.defaults?.model ?? {},
130
+ providers: redactSecrets(config.models?.providers ?? {})
131
+ }
132
+ };
133
+ };
134
+
135
+ function inferProviderFromPrimaryModel(primaryModel: string): string | null {
136
+ const slashIndex = primaryModel.indexOf('/');
137
+ if (slashIndex <= 0) {
138
+ return null;
139
+ }
140
+
141
+ return primaryModel.slice(0, slashIndex);
142
+ }
143
+
144
+ function pickString(value: JsonValue | undefined): string | null {
145
+ if (typeof value !== 'string') {
146
+ return null;
147
+ }
148
+
149
+ const trimmed = value.trim();
150
+ return trimmed.length > 0 ? trimmed : null;
151
+ }
152
+
153
+ function isObject(value: JsonValue | undefined): value is Record<string, JsonValue> {
154
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
155
+ }
156
+
157
+ function redactSecrets(value: any): any {
158
+ if (Array.isArray(value)) {
159
+ return value.map(redactSecrets);
160
+ }
161
+
162
+ if (!value || typeof value !== 'object') {
163
+ return value;
164
+ }
165
+
166
+ const result: Record<string, any> = {};
167
+
168
+ for (const [key, nestedValue] of Object.entries(value)) {
169
+ if (isSecretKey(key) && typeof nestedValue === 'string') {
170
+ result[key] = redactString(nestedValue);
171
+ continue;
172
+ }
173
+
174
+ result[key] = redactSecrets(nestedValue);
175
+ }
176
+
177
+ return result;
178
+ }
179
+
180
+ function isSecretKey(key: string): boolean {
181
+ const normalized = key.toLowerCase();
182
+ return normalized.includes('apikey') || normalized.includes('token') || normalized.includes('secret');
183
+ }
184
+
185
+ function redactString(value: string): string {
186
+ if (value.length <= 8) {
187
+ return '********';
188
+ }
189
+
190
+ return `${value.slice(0, 4)}***${value.slice(-4)}`;
191
+ }
@@ -0,0 +1,103 @@
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
+ models?: {
8
+ mode?: string;
9
+ providers?: JsonValue;
10
+ };
11
+ agents?: {
12
+ defaults?: {
13
+ model?: {
14
+ primary?: string;
15
+ provider?: string;
16
+ [key: string]: any;
17
+ };
18
+ [key: string]: any;
19
+ };
20
+ list?: Array<{
21
+ id?: string;
22
+ name?: string;
23
+ model?: {
24
+ primary?: string;
25
+ provider?: string;
26
+ [key: string]: any;
27
+ };
28
+ [key: string]: any;
29
+ }>;
30
+ };
31
+ }
32
+
33
+ export const getModels: MethodHandler = async (_params, context): Promise<JsonValue> => {
34
+ const configPath = path.join(context.openclawRoot, 'openclaw.json');
35
+ const config = await readJsonFile<OpenClawConfig>(configPath);
36
+
37
+ return {
38
+ sourceConfigFile: configPath,
39
+ defaults: {
40
+ model: config.agents?.defaults?.model ?? {}
41
+ },
42
+ agents: normalizeAgentModels(config),
43
+ modelConfigMode: config.models?.mode ?? null,
44
+ configuredProviders: redactSecrets(config.models?.providers ?? {})
45
+ };
46
+ };
47
+
48
+ function normalizeAgentModels(config: OpenClawConfig): JsonValue[] {
49
+ const items: JsonValue[] = [
50
+ {
51
+ id: 'defaults',
52
+ name: 'defaults',
53
+ model: config.agents?.defaults?.model ?? {}
54
+ }
55
+ ];
56
+
57
+ const agents = Array.isArray(config.agents?.list) ? config.agents.list : [];
58
+ for (const agent of agents) {
59
+ items.push({
60
+ id: agent.id ?? null,
61
+ name: agent.name ?? agent.id ?? null,
62
+ model: agent.model ?? {}
63
+ });
64
+ }
65
+
66
+ return items;
67
+ }
68
+
69
+ function redactSecrets(value: JsonValue): JsonValue {
70
+ if (Array.isArray(value)) {
71
+ return value.map(redactSecrets);
72
+ }
73
+
74
+ if (!value || typeof value !== 'object') {
75
+ return value;
76
+ }
77
+
78
+ const result: Record<string, JsonValue> = {};
79
+
80
+ for (const [key, nestedValue] of Object.entries(value)) {
81
+ if (isSecretKey(key) && typeof nestedValue === 'string') {
82
+ result[key] = redactString(nestedValue);
83
+ continue;
84
+ }
85
+
86
+ result[key] = redactSecrets(nestedValue);
87
+ }
88
+
89
+ return result;
90
+ }
91
+
92
+ function isSecretKey(key: string): boolean {
93
+ const normalized = key.toLowerCase();
94
+ return normalized.includes('apikey') || normalized.includes('token') || normalized.includes('secret');
95
+ }
96
+
97
+ function redactString(value: string): string {
98
+ if (value.length <= 8) {
99
+ return '********';
100
+ }
101
+
102
+ return `${value.slice(0, 4)}***${value.slice(-4)}`;
103
+ }
@@ -0,0 +1,313 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import readline from 'node:readline';
4
+
5
+ import { pathExists, readJsonFile } from '../lib/fs.ts';
6
+ import { JsonRpcException, JSON_RPC_ERRORS } from '../jsonrpc.ts';
7
+ import type { JsonValue, MethodContext, MethodHandler } from '../types.ts';
8
+ import { findSessionRecord } from './sessions.ts';
9
+
10
+ interface SessionMessage {
11
+ role: string;
12
+ content: string;
13
+ timestamp?: number;
14
+ [key: string]: any;
15
+ }
16
+
17
+ const MAX_SESSION_MESSAGES = 100;
18
+
19
+ export const getSession: MethodHandler = async (
20
+ params,
21
+ context
22
+ ): Promise<JsonValue> => {
23
+ const objectParams = isObject(params) ? params : {};
24
+ const sessionId = typeof objectParams.sessionId === 'string' ? objectParams.sessionId : null;
25
+ const requestedLimit = typeof objectParams.limit === 'number' ? objectParams.limit : MAX_SESSION_MESSAGES;
26
+ const requestedOffset = typeof objectParams.offset === 'number' ? objectParams.offset : 0;
27
+ const limit = normalizePageSize(requestedLimit, MAX_SESSION_MESSAGES);
28
+ const offset = normalizeOffset(requestedOffset);
29
+
30
+ if (!sessionId) {
31
+ throw new JsonRpcException(
32
+ JSON_RPC_ERRORS.invalidParams,
33
+ 'Missing required parameter: sessionId'
34
+ );
35
+ }
36
+
37
+ const session = await findSessionRecord(context, sessionId);
38
+ if (!session) {
39
+ throw new JsonRpcException(
40
+ JSON_RPC_ERRORS.invalidParams,
41
+ `Session not found: ${sessionId}`
42
+ );
43
+ }
44
+
45
+ const sessionFile = session.sessionFilePath ?? path.join(
46
+ context.openclawRoot,
47
+ 'agents',
48
+ session.agentName,
49
+ 'sessions',
50
+ `${session.sessionId ?? sessionId}.jsonl`
51
+ );
52
+ const messages = await readSessionMessages(sessionFile, limit, offset);
53
+
54
+ return {
55
+ agentName: session.agentName,
56
+ sessionId: session.sessionId ?? sessionId,
57
+ sessionKey: session.sessionKey,
58
+ updatedAt: session.updatedAt,
59
+ status: session.status,
60
+ provider: session.provider,
61
+ model: session.model,
62
+ workspaceDir: session.workspaceDir,
63
+ originLabel: session.originLabel,
64
+ chatType: session.chatType,
65
+ messages: {
66
+ total: messages.total,
67
+ limit,
68
+ offset,
69
+ items: messages.items
70
+ }
71
+ };
72
+ };
73
+
74
+ export const prepareMessage: MethodHandler = async (
75
+ params,
76
+ context
77
+ ): Promise<JsonValue> => {
78
+ const objectParams = isObject(params) ? params : {};
79
+ const sessionId = typeof objectParams.sessionId === 'string' ? objectParams.sessionId : null;
80
+ const message = typeof objectParams.message === 'string' ? objectParams.message : null;
81
+ const attachedSkills = Array.isArray(objectParams.attachedSkills) ? objectParams.attachedSkills : [];
82
+
83
+ if (!sessionId) {
84
+ throw new JsonRpcException(
85
+ JSON_RPC_ERRORS.invalidParams,
86
+ 'Missing required parameter: sessionId'
87
+ );
88
+ }
89
+
90
+ if (!message) {
91
+ throw new JsonRpcException(
92
+ JSON_RPC_ERRORS.invalidParams,
93
+ 'Missing required parameter: message'
94
+ );
95
+ }
96
+
97
+ const session = await findSessionRecord(context, sessionId);
98
+ if (!session) {
99
+ throw new JsonRpcException(
100
+ JSON_RPC_ERRORS.invalidParams,
101
+ `Session not found: ${sessionId}`
102
+ );
103
+ }
104
+
105
+ const messageData: any = {
106
+ agentName: session.agentName,
107
+ sessionId: session.sessionId ?? sessionId,
108
+ sessionKey: session.sessionKey,
109
+ message,
110
+ timestamp: Date.now()
111
+ };
112
+
113
+ if (attachedSkills.length > 0) {
114
+ messageData.attachedSkills = attachedSkills;
115
+ const skillContexts = await loadSkillContexts(context, attachedSkills);
116
+ if (skillContexts.length > 0) {
117
+ messageData.skillContexts = skillContexts;
118
+ messageData.enhancedMessage = buildEnhancedMessage(message, skillContexts);
119
+ }
120
+ }
121
+
122
+ return {
123
+ success: true,
124
+ messageData,
125
+ note: 'Message data prepared. To actually send, use MQTT sender message or OpenClaw runtime API.'
126
+ };
127
+ };
128
+
129
+ export const attachSkill: MethodHandler = async (
130
+ params,
131
+ context
132
+ ): Promise<JsonValue> => {
133
+ const objectParams = isObject(params) ? params : {};
134
+ const sessionId = typeof objectParams.sessionId === 'string' ? objectParams.sessionId : null;
135
+ const skillSlug = typeof objectParams.skillSlug === 'string' ? objectParams.skillSlug : null;
136
+ const message = typeof objectParams.message === 'string' ? objectParams.message : '';
137
+
138
+ if (!sessionId) {
139
+ throw new JsonRpcException(
140
+ JSON_RPC_ERRORS.invalidParams,
141
+ 'Missing required parameter: sessionId'
142
+ );
143
+ }
144
+
145
+ if (!skillSlug) {
146
+ throw new JsonRpcException(
147
+ JSON_RPC_ERRORS.invalidParams,
148
+ 'Missing required parameter: skillSlug'
149
+ );
150
+ }
151
+
152
+ const skillPath = await findInstalledSkill(context, skillSlug);
153
+ if (!skillPath) {
154
+ throw new JsonRpcException(
155
+ JSON_RPC_ERRORS.invalidParams,
156
+ `Skill not installed: ${skillSlug}`
157
+ );
158
+ }
159
+
160
+ const skillContext = await loadSkillContext(context, skillSlug, skillPath);
161
+ const enhancedMessage = message
162
+ ? `${message}\n\n[Attached Skill: ${skillSlug}]`
163
+ : `[Using Skill: ${skillSlug}]`;
164
+
165
+ return {
166
+ success: true,
167
+ sessionId,
168
+ skillSlug,
169
+ skillContext,
170
+ enhancedMessage,
171
+ messageData: {
172
+ sessionId,
173
+ message: enhancedMessage,
174
+ attachedSkills: [{ slug: skillSlug }],
175
+ skillContexts: [skillContext],
176
+ timestamp: Date.now()
177
+ }
178
+ };
179
+ };
180
+
181
+ function isObject(value: JsonValue | undefined): value is Record<string, JsonValue> {
182
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
183
+ }
184
+
185
+ function normalizePageSize(value: number, max: number): number {
186
+ if (!Number.isFinite(value)) {
187
+ return max;
188
+ }
189
+
190
+ const normalized = Math.floor(value);
191
+ if (normalized <= 0) {
192
+ return max;
193
+ }
194
+
195
+ return Math.min(normalized, max);
196
+ }
197
+
198
+ function normalizeOffset(value: number): number {
199
+ if (!Number.isFinite(value)) {
200
+ return 0;
201
+ }
202
+
203
+ return Math.max(0, Math.floor(value));
204
+ }
205
+
206
+ async function readSessionMessages(
207
+ filePath: string,
208
+ limit: number,
209
+ offset: number
210
+ ): Promise<{ total: number; items: SessionMessage[] }> {
211
+ if (!(await pathExists(filePath))) {
212
+ return { total: 0, items: [] };
213
+ }
214
+
215
+ const messages: SessionMessage[] = [];
216
+ const fileStream = fs.createReadStream(filePath);
217
+ const rl = readline.createInterface({
218
+ input: fileStream,
219
+ crlfDelay: Infinity
220
+ });
221
+
222
+ for await (const line of rl) {
223
+ if (!line.trim()) {
224
+ continue;
225
+ }
226
+
227
+ try {
228
+ messages.push(JSON.parse(line));
229
+ } catch {
230
+ continue;
231
+ }
232
+ }
233
+
234
+ return {
235
+ total: messages.length,
236
+ items: messages.slice(offset, offset + limit)
237
+ };
238
+ }
239
+
240
+ async function findInstalledSkill(
241
+ context: MethodContext,
242
+ skillSlug: string
243
+ ): Promise<string | null> {
244
+ const globalPath = path.join(context.openclawRoot, 'skills', skillSlug);
245
+ if (await pathExists(globalPath)) {
246
+ return globalPath;
247
+ }
248
+
249
+ const workspacePath = path.join(context.openclawRoot, 'workspace', '.openclaw', 'skills', skillSlug);
250
+ if (await pathExists(workspacePath)) {
251
+ return workspacePath;
252
+ }
253
+
254
+ return null;
255
+ }
256
+
257
+ async function loadSkillContext(
258
+ context: MethodContext,
259
+ skillSlug: string,
260
+ skillPath: string
261
+ ): Promise<any> {
262
+ const skillMdPath = path.join(skillPath, 'SKILL.md');
263
+ let content = '';
264
+
265
+ if (await pathExists(skillMdPath)) {
266
+ content = await fs.promises.readFile(skillMdPath, 'utf-8');
267
+ }
268
+
269
+ const skillJsonPath = path.join(skillPath, 'skill.json');
270
+ let metadata: any = { slug: skillSlug };
271
+
272
+ if (await pathExists(skillJsonPath)) {
273
+ metadata = await readJsonFile(skillJsonPath);
274
+ }
275
+
276
+ return {
277
+ slug: skillSlug,
278
+ path: skillPath,
279
+ content,
280
+ metadata
281
+ };
282
+ }
283
+
284
+ async function loadSkillContexts(
285
+ context: MethodContext,
286
+ skills: any[]
287
+ ): Promise<any[]> {
288
+ const contexts: any[] = [];
289
+
290
+ for (const skill of skills) {
291
+ const skillSlug = typeof skill === 'string' ? skill : skill?.slug;
292
+ if (!skillSlug) {
293
+ continue;
294
+ }
295
+
296
+ const skillPath = await findInstalledSkill(context, skillSlug);
297
+ if (!skillPath) {
298
+ continue;
299
+ }
300
+
301
+ contexts.push(await loadSkillContext(context, skillSlug, skillPath));
302
+ }
303
+
304
+ return contexts;
305
+ }
306
+
307
+ function buildEnhancedMessage(message: string, skillContexts: any[]): string {
308
+ const skillSections = skillContexts
309
+ .map((ctx) => `\n\n--- Skill: ${ctx.slug} ---\n${ctx.content}`)
310
+ .join('\n');
311
+
312
+ return `${message}${skillSections}`;
313
+ }