rol-websocket-channel 1.1.2 → 1.1.4

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.
@@ -1,4 +1,4 @@
1
- # OpenClaw MQTT API 文档 (配对逻辑更新)
1
+ # OpenClaw MQTT API 文档 (新增文件功能)
2
2
 
3
3
  ## 消息格式
4
4
 
@@ -1671,7 +1671,7 @@ openclaw admin-bridge pair <key> --endpoint https://api.deotaland.ai
1671
1671
 
1672
1672
  MQTT 请求示例:
1673
1673
 
1674
- ```json
1674
+ ​```json
1675
1675
  {
1676
1676
  "type": "artifactsList",
1677
1677
  "trace_id": "artifact-list-before-download-001",
package/index.ts CHANGED
@@ -507,7 +507,15 @@ async function handleIncomingMessage(
507
507
  timestamp: Date.now(),
508
508
  };
509
509
 
510
- conn.ws.publish(mqttTopic, JSON.stringify(replyMessage));
510
+ // 根据 source_type 修改 topic 末尾的 #
511
+ let targetTopic = mqttTopic;
512
+ const sourceType = innerData?.source_type;
513
+ if (targetTopic.endsWith("#")) {
514
+ const replacement = sourceType === "device" ? "device" : "bot";
515
+ targetTopic = targetTopic.slice(0, -1) + replacement;
516
+ }
517
+
518
+ conn.ws.publish(targetTopic, JSON.stringify(replyMessage));
511
519
  },
512
520
  onError: (err: Error) => {
513
521
  log?.error(`[rol-websocket-channel] Delivery error: ${err.message}`);
@@ -528,6 +536,7 @@ async function handleCustomMessageType(
528
536
  accountId: string,
529
537
  mqttTopic: string,
530
538
  ): Promise<void> {
539
+ const isSkillInstallFlow = msgType === "skillsInstallFromClawHub" || msgType === "skillsUpdateFromClawHub";
531
540
  const response: any = {
532
541
  type: "receiver",
533
542
  trace_id: traceId,
@@ -535,6 +544,12 @@ async function handleCustomMessageType(
535
544
  timestamp: Date.now(),
536
545
  };
537
546
 
547
+ if (isSkillInstallFlow) {
548
+ console.log(
549
+ `[rol-websocket-channel] custom message start: type=${msgType}, traceId=${traceId}, slug=${innerData?.slug ?? ""}, accountId=${accountId}, topic=${mqttTopic}`,
550
+ );
551
+ }
552
+
538
553
  const handlerMethod = (messageHandler as any)[msgType];
539
554
  if (typeof handlerMethod === "function") {
540
555
  try {
@@ -553,15 +568,34 @@ async function handleCustomMessageType(
553
568
  response.data = methodResult.result;
554
569
  if (!methodResult.ok) {
555
570
  response.error = methodResult.error?.message || "Unknown error";
571
+ if (isSkillInstallFlow) {
572
+ console.error(
573
+ `[rol-websocket-channel] custom message failed: type=${msgType}, traceId=${traceId}, slug=${innerData?.slug ?? ""}, error=${response.error}, detail=${JSON.stringify(methodResult.error?.data ?? {})}`,
574
+ );
575
+ }
576
+ } else if (isSkillInstallFlow) {
577
+ console.log(
578
+ `[rol-websocket-channel] custom message success: type=${msgType}, traceId=${traceId}, slug=${innerData?.slug ?? ""}`,
579
+ );
556
580
  }
557
581
  } else {
558
582
  // 旧格式:直接返回数据
559
583
  response.success = true;
560
584
  response.data = methodResult;
585
+ if (isSkillInstallFlow) {
586
+ console.log(
587
+ `[rol-websocket-channel] custom message success: type=${msgType}, traceId=${traceId}, slug=${innerData?.slug ?? ""}`,
588
+ );
589
+ }
561
590
  }
562
591
  } catch (handlerErr: any) {
563
592
  response.success = false;
564
593
  response.error = handlerErr.message;
594
+ if (isSkillInstallFlow) {
595
+ console.error(
596
+ `[rol-websocket-channel] custom message threw: type=${msgType}, traceId=${traceId}, slug=${innerData?.slug ?? ""}, error=${handlerErr?.message ?? String(handlerErr)}`,
597
+ );
598
+ }
565
599
  }
566
600
  } else {
567
601
  response.success = false;
@@ -570,7 +604,15 @@ async function handleCustomMessageType(
570
604
 
571
605
  const conn = ConnectionManager.getGlobalConnection();
572
606
  if (conn && conn.ws && conn.ws.connected) {
573
- conn.ws.publish(mqttTopic, JSON.stringify(response));
607
+ // 根据 source_type 修改 topic 末尾的 #
608
+ let targetTopic = mqttTopic;
609
+ const sourceType = innerData?.source_type;
610
+ if (targetTopic.endsWith("#")) {
611
+ const replacement = sourceType === "device" ? "device" : "bot";
612
+ targetTopic = targetTopic.slice(0, -1) + replacement;
613
+ }
614
+
615
+ conn.ws.publish(targetTopic, JSON.stringify(response));
574
616
  }
575
617
  }
576
618
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rol-websocket-channel",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
4
4
  "description": "Unified OpenClaw plugin: MQTT Channel + Admin Bridge for remote management",
5
5
  "license": "MIT",
6
6
  "author": "nixgnehc",
@@ -21,6 +21,9 @@ export const getSession: MethodHandler = async (
21
21
  context
22
22
  ): Promise<JsonValue> => {
23
23
  const objectParams = isObject(params) ? params : {};
24
+ const agentId = typeof objectParams.agentId === 'string' && objectParams.agentId.trim()
25
+ ? objectParams.agentId.trim()
26
+ : undefined;
24
27
  const sessionId = typeof objectParams.sessionId === 'string' ? objectParams.sessionId : null;
25
28
  const requestedLimit = typeof objectParams.limit === 'number' ? objectParams.limit : MAX_SESSION_MESSAGES;
26
29
  const requestedOffset = typeof objectParams.offset === 'number' ? objectParams.offset : 0;
@@ -34,11 +37,11 @@ export const getSession: MethodHandler = async (
34
37
  );
35
38
  }
36
39
 
37
- const session = await findSessionRecord(context, sessionId);
40
+ const session = await findSessionRecord(context, sessionId, agentId);
38
41
  if (!session) {
39
42
  throw new JsonRpcException(
40
43
  JSON_RPC_ERRORS.invalidParams,
41
- `Session not found: ${sessionId}`
44
+ agentId ? `Session not found: ${sessionId} for agent ${agentId}` : `Session not found: ${sessionId}`
42
45
  );
43
46
  }
44
47
 
@@ -52,6 +55,7 @@ export const getSession: MethodHandler = async (
52
55
  const messages = await readSessionMessages(sessionFile, limit, offset);
53
56
 
54
57
  return {
58
+ agentId: session.agentId,
55
59
  agentName: session.agentName,
56
60
  sessionId: session.sessionId ?? sessionId,
57
61
  sessionKey: session.sessionKey,
@@ -76,6 +80,9 @@ export const prepareMessage: MethodHandler = async (
76
80
  context
77
81
  ): Promise<JsonValue> => {
78
82
  const objectParams = isObject(params) ? params : {};
83
+ const agentId = typeof objectParams.agentId === 'string' && objectParams.agentId.trim()
84
+ ? objectParams.agentId.trim()
85
+ : undefined;
79
86
  const sessionId = typeof objectParams.sessionId === 'string' ? objectParams.sessionId : null;
80
87
  const message = typeof objectParams.message === 'string' ? objectParams.message : null;
81
88
  const attachedSkills = Array.isArray(objectParams.attachedSkills) ? objectParams.attachedSkills : [];
@@ -94,15 +101,16 @@ export const prepareMessage: MethodHandler = async (
94
101
  );
95
102
  }
96
103
 
97
- const session = await findSessionRecord(context, sessionId);
104
+ const session = await findSessionRecord(context, sessionId, agentId);
98
105
  if (!session) {
99
106
  throw new JsonRpcException(
100
107
  JSON_RPC_ERRORS.invalidParams,
101
- `Session not found: ${sessionId}`
108
+ agentId ? `Session not found: ${sessionId} for agent ${agentId}` : `Session not found: ${sessionId}`
102
109
  );
103
110
  }
104
111
 
105
112
  const messageData: any = {
113
+ agentId: session.agentId,
106
114
  agentName: session.agentName,
107
115
  sessionId: session.sessionId ?? sessionId,
108
116
  sessionKey: session.sessionKey,
@@ -26,6 +26,7 @@ interface SessionsIndexEntry {
26
26
  type SessionsIndex = Record<string, SessionsIndexEntry>;
27
27
 
28
28
  export interface AgentSessionRecord {
29
+ agentId: string;
29
30
  agentName: string;
30
31
  sessionKey: string;
31
32
  sessionId: string | null;
@@ -41,17 +42,24 @@ export interface AgentSessionRecord {
41
42
  }
42
43
 
43
44
  export const listSessions: MethodHandler = async (
44
- _params,
45
+ params,
45
46
  context
46
47
  ): Promise<JsonValue> => {
47
- const items = await listAllAgentSessions(context);
48
+ const objectParams = isObject(params) ? params : {};
49
+ const agentId = typeof objectParams.agentId === 'string' && objectParams.agentId.trim()
50
+ ? objectParams.agentId.trim()
51
+ : undefined;
52
+ const items = await listAllAgentSessions(context, agentId);
48
53
  return {
49
54
  count: items.length,
50
55
  items
51
56
  };
52
57
  };
53
58
 
54
- export async function listAllAgentSessions(context: MethodContext): Promise<AgentSessionRecord[]> {
59
+ export async function listAllAgentSessions(
60
+ context: MethodContext,
61
+ agentId?: string
62
+ ): Promise<AgentSessionRecord[]> {
55
63
  const agentsRoot = path.join(context.openclawRoot, 'agents');
56
64
  if (!(await pathExists(agentsRoot))) {
57
65
  return [];
@@ -66,6 +74,10 @@ export async function listAllAgentSessions(context: MethodContext): Promise<Agen
66
74
  }
67
75
 
68
76
  const agentName = agentEntry.name;
77
+ if (agentId && agentName !== agentId) {
78
+ continue;
79
+ }
80
+
69
81
  const sessionsPath = path.join(agentsRoot, agentName, 'sessions', 'sessions.json');
70
82
  if (!(await pathExists(sessionsPath))) {
71
83
  continue;
@@ -75,6 +87,7 @@ export async function listAllAgentSessions(context: MethodContext): Promise<Agen
75
87
  for (const [sessionKey, entry] of Object.entries(sessions)) {
76
88
  const sessionId = entry.sessionId ?? extractSessionIdFromFile(entry.sessionFile) ?? null;
77
89
  items.push({
90
+ agentId: agentName,
78
91
  agentName,
79
92
  sessionKey,
80
93
  sessionId,
@@ -98,20 +111,26 @@ export async function listAllAgentSessions(context: MethodContext): Promise<Agen
98
111
 
99
112
  export async function findSessionRecord(
100
113
  context: MethodContext,
101
- sessionIdOrKey: string
114
+ sessionIdOrKey: string,
115
+ agentId?: string
102
116
  ): Promise<AgentSessionRecord | null> {
103
- const sessions = await listAllAgentSessions(context);
117
+ const sessions = await listAllAgentSessions(context, agentId);
104
118
  return sessions.find((item) => item.sessionId === sessionIdOrKey || item.sessionKey === sessionIdOrKey) ?? null;
105
119
  }
106
120
 
107
121
  export async function resolveSessionFile(
108
122
  context: MethodContext,
109
- sessionIdOrKey: string
123
+ sessionIdOrKey: string,
124
+ agentId?: string
110
125
  ): Promise<string | null> {
111
- const session = await findSessionRecord(context, sessionIdOrKey);
126
+ const session = await findSessionRecord(context, sessionIdOrKey, agentId);
112
127
  return session?.sessionFilePath ?? null;
113
128
  }
114
129
 
130
+ function isObject(value: JsonValue | undefined): value is Record<string, JsonValue> {
131
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
132
+ }
133
+
115
134
  function extractSessionIdFromFile(sessionFile: string | undefined): string | null {
116
135
  if (!sessionFile) {
117
136
  return null;
@@ -143,7 +143,7 @@ export const searchClawHubSkills: MethodHandler = async (params, context): Promi
143
143
  const objectParams = isObject(params) ? params : {};
144
144
  const query = typeof objectParams.query === 'string' ? objectParams.query.trim() : '';
145
145
  const args = query ? ['skills', 'search', query, '--json'] : ['skills', 'search', '--json'];
146
- const result = await runOpenClawSkillCommand(args, context.projectRoot);
146
+ const result = await runOpenClawSkillCommand(args, context.openclawRoot, context.openclawRoot);
147
147
 
148
148
  return {
149
149
  ok: true,
@@ -155,7 +155,7 @@ export const searchClawHubSkills: MethodHandler = async (params, context): Promi
155
155
  export const installSkillFromClawHub: MethodHandler = async (params, context): Promise<JsonValue> => {
156
156
  const objectParams = expectObject(params) as unknown as ClawHubSkillParams;
157
157
  const slug = expectString(objectParams.slug, 'slug');
158
- const result = await runOpenClawSkillCommand(['skills', 'install', slug], context.projectRoot);
158
+ const result = await runOpenClawSkillCommand(['skills', 'install', slug], context.openclawRoot, context.openclawRoot);
159
159
 
160
160
  return {
161
161
  ok: true,
@@ -167,7 +167,7 @@ export const installSkillFromClawHub: MethodHandler = async (params, context): P
167
167
  export const updateSkillFromClawHub: MethodHandler = async (params, context): Promise<JsonValue> => {
168
168
  const objectParams = expectObject(params) as unknown as ClawHubSkillParams;
169
169
  const slug = expectString(objectParams.slug, 'slug');
170
- const result = await runOpenClawSkillCommand(['skills', 'update', slug], context.projectRoot);
170
+ const result = await runOpenClawSkillCommand(['skills', 'update', slug], context.openclawRoot, context.openclawRoot);
171
171
 
172
172
  return {
173
173
  ok: true,
@@ -233,27 +233,42 @@ async function npmPack(packageSpec: string, cwd: string): Promise<string> {
233
233
 
234
234
  async function runOpenClawSkillCommand(
235
235
  args: string[],
236
- cwd: string
236
+ cwd: string,
237
+ openclawRoot?: string
237
238
  ): Promise<{ stdout: string; stderr: string; parsed: JsonValue | null }> {
238
239
  const command = process.env.OPENCLAW_BIN || 'openclaw';
239
- const options = buildOpenClawExecOptions(cwd);
240
+ const options = buildOpenClawExecOptions(cwd, openclawRoot);
241
+ const openclawBin = process.env.OPENCLAW_BIN || '';
242
+ const openclawHome = options.env?.OPENCLAW_HOME || '';
243
+
244
+ console.log(
245
+ `[skills] exec start: command=${command}, args=${JSON.stringify(args)}, cwd=${cwd}, OPENCLAW_BIN=${openclawBin}, OPENCLAW_HOME=${openclawHome}`
246
+ );
240
247
 
241
248
  try {
242
249
  const { stdout, stderr } = await execFileAsync(command, args, options);
250
+ console.log(
251
+ `[skills] exec success: command=${command}, args=${JSON.stringify(args)}, stdoutLength=${stdout.length}, stderrLength=${stderr.length}`
252
+ );
243
253
  return {
244
254
  stdout,
245
255
  stderr,
246
256
  parsed: parseJsonOutput(stdout)
247
257
  };
248
258
  } catch (err: any) {
259
+ const stdout = typeof err?.stdout === 'string' ? err.stdout : '';
260
+ const stderr = typeof err?.stderr === 'string' ? err.stderr : '';
261
+ console.error(
262
+ `[skills] exec failed: command=${command}, args=${JSON.stringify(args)}, cwd=${cwd}, OPENCLAW_BIN=${openclawBin}, OPENCLAW_HOME=${openclawHome}, stdout=${JSON.stringify(stdout)}, stderr=${JSON.stringify(stderr)}`
263
+ );
249
264
  throw new JsonRpcException(
250
265
  JSON_RPC_ERRORS.internalError,
251
266
  `OpenClaw skill command failed: ${err instanceof Error ? err.message : String(err)}`,
252
267
  {
253
268
  command,
254
269
  args,
255
- stdout: typeof err?.stdout === 'string' ? err.stdout : '',
256
- stderr: typeof err?.stderr === 'string' ? err.stderr : ''
270
+ stdout,
271
+ stderr
257
272
  }
258
273
  );
259
274
  }
@@ -274,7 +289,7 @@ function parseJsonOutput(stdout: string): JsonValue | null {
274
289
 
275
290
  async function queryOpenClawSkills(context: MethodContext): Promise<unknown[]> {
276
291
  const command = process.env.OPENCLAW_BIN || 'openclaw';
277
- const options = buildOpenClawExecOptions(context.projectRoot);
292
+ const options = buildOpenClawExecOptions(context.openclawRoot, context.openclawRoot);
278
293
 
279
294
  try {
280
295
  const { stdout } = await execFileAsync(command, ['skills', 'list', '--json'], options);
@@ -300,15 +315,38 @@ async function queryOpenClawSkills(context: MethodContext): Promise<unknown[]> {
300
315
  }
301
316
  }
302
317
 
303
- function buildOpenClawExecOptions(cwd: string): { cwd: string; shell?: boolean } {
318
+ function buildOpenClawExecOptions(
319
+ cwd: string,
320
+ openclawRoot?: string
321
+ ): { cwd: string; shell?: boolean; env: NodeJS.ProcessEnv } {
322
+ const env: NodeJS.ProcessEnv = { ...process.env };
323
+ const openclawHome = resolveOpenClawHomeForCli(openclawRoot);
324
+ if (openclawHome) {
325
+ env.OPENCLAW_HOME = openclawHome;
326
+ }
327
+
304
328
  if (process.platform === 'win32') {
305
329
  return {
306
330
  cwd,
307
- shell: true
331
+ shell: true,
332
+ env
308
333
  };
309
334
  }
310
335
 
311
- return { cwd };
336
+ return { cwd, env };
337
+ }
338
+
339
+ function resolveOpenClawHomeForCli(openclawRoot?: string): string | undefined {
340
+ if (!openclawRoot) {
341
+ return process.env.OPENCLAW_HOME;
342
+ }
343
+
344
+ const resolvedRoot = path.resolve(openclawRoot);
345
+ if (path.basename(resolvedRoot) === '.openclaw') {
346
+ return path.dirname(resolvedRoot);
347
+ }
348
+
349
+ return process.env.OPENCLAW_HOME;
312
350
  }
313
351
 
314
352
  function normalizeCliSkill(
@@ -136,9 +136,9 @@ export const logs: MethodHandler = async (params: any, context: MethodContext):
136
136
  return { ok: false, error: `No .log files found in directory: ${logDir}` };
137
137
  }
138
138
 
139
- const limit = params?.limit ?? 200;
139
+ const limit = 10;
140
140
  const maxBytes = params?.maxBytes ?? 250000;
141
- let offset = params?.offset;
141
+ const offset = undefined;
142
142
 
143
143
  // 获取所有候选日志的详细信息并排序(对应 ls -t)
144
144
  const fileStats = await Promise.all(
@@ -180,7 +180,9 @@ export const logs: MethodHandler = async (params: any, context: MethodContext):
180
180
  const content = buffer.toString('utf-8');
181
181
  const rawLines = content.split(/\r?\n/).filter(line => line.trim().length > 0);
182
182
 
183
- const resultLines = typeof offset === 'number' ? rawLines : rawLines.slice(-limit);
183
+ const resultLines = typeof offset === 'number'
184
+ ? rawLines
185
+ : rawLines.slice(-limit).reverse();
184
186
 
185
187
  return {
186
188
  ok: true,