rol-websocket-channel 1.7.3 → 1.7.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +45 -6
- package/dist/src/admin/lib/paths.js +10 -10
- package/dist/src/admin/methods/pairing.js +66 -62
- package/dist/src/admin/methods/sessions-extended.js +19 -13
- package/dist/src/admin/methods/system.js +116 -3
- package/index.ts +54 -7
- package/package.json +1 -1
- package/src/admin/lib/paths.ts +10 -10
- package/src/admin/methods/pairing.ts +79 -66
- package/src/admin/methods/sessions-extended.ts +25 -16
- package/src/admin/methods/system.ts +149 -3
- package/tsconfig.json +1 -1
- package/dist/src/admin/cli-manifest.test.js +0 -122
- package/dist/src/admin/lib/openclaw-bin.test.js +0 -37
- package/dist/src/admin/methods/admin.test.js +0 -61
- package/dist/src/admin/methods/artifacts.test.js +0 -304
- package/dist/src/admin/methods/index.test.js +0 -27
- package/dist/src/admin/methods/mem9.test.js +0 -259
- package/dist/src/admin/methods/pairing.test.js +0 -114
- package/dist/src/admin/tools/artifacts-tools.test.js +0 -163
- package/dist/src/mqtt/mqtt.test.js +0 -418
- package/src/admin/cli-manifest.test.ts +0 -135
- package/src/admin/lib/openclaw-bin.test.ts +0 -38
- package/src/admin/methods/admin.test.ts +0 -91
- package/src/admin/methods/artifacts.test.ts +0 -486
- package/src/admin/methods/index.test.ts +0 -34
- package/src/admin/methods/mem9.test.ts +0 -318
- package/src/admin/methods/pairing.test.ts +0 -149
- package/src/admin/tools/artifacts-tools.test.ts +0 -215
- package/src/mqtt/mqtt.test.ts +0 -670
- package/test/admin/methods/models-extended.test.ts +0 -49
- package/test/message-handler-artifacts.test.ts +0 -11
package/dist/index.js
CHANGED
|
@@ -34,16 +34,30 @@ function getChannelConfig(cfg) {
|
|
|
34
34
|
export function getPluginRuntime() {
|
|
35
35
|
return pluginRuntime;
|
|
36
36
|
}
|
|
37
|
+
// Chinese translations keyed by error code, so CLI failures are returned
|
|
38
|
+
// bilingually (English `message` + Chinese `messageZh`).
|
|
39
|
+
const ERROR_MESSAGES_ZH = {
|
|
40
|
+
PAIR_KEY_REQUIRED: "配对码不能为空",
|
|
41
|
+
PAIR_CONFIG_NOT_FOUND: "找不到 openclaw.json 配置文件(请确认插件位于 OpenClaw 安装目录内,或设置环境变量 OPENCLAW_HOME 指向 .openclaw 目录)",
|
|
42
|
+
PAIR_EXCHANGE_REQUEST_FAILED: "配对请求发送失败(无法连接配对服务,请检查网络、DNS 或代理)",
|
|
43
|
+
PAIR_EXCHANGE_FAILED: "配对兑换被拒绝(配对码无效、已过期或已被使用;请向签发服务获取新配对码,并在 10 分钟内尽快完成配对)",
|
|
44
|
+
PAIR_CHANNEL_CONFIG_INVALID: "配对返回的频道配置无效(缺少 mqttUrl 或 mqttTopic)",
|
|
45
|
+
};
|
|
37
46
|
export function formatCliErrorPayload(error) {
|
|
38
47
|
const data = error?.data;
|
|
39
48
|
const dataObject = data && typeof data === "object" && !Array.isArray(data)
|
|
40
49
|
? data
|
|
41
50
|
: {};
|
|
42
51
|
const code = dataObject.code ?? error?.code;
|
|
52
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
53
|
+
// Fall back to the English message when no Chinese translation exists for
|
|
54
|
+
// the code, so both fields are always present.
|
|
55
|
+
const messageZh = (typeof code === "string" && ERROR_MESSAGES_ZH[code]) || message;
|
|
43
56
|
return {
|
|
44
57
|
ok: false,
|
|
45
58
|
error: {
|
|
46
|
-
message
|
|
59
|
+
message,
|
|
60
|
+
messageZh,
|
|
47
61
|
...dataObject,
|
|
48
62
|
...(code === undefined ? {} : { code }),
|
|
49
63
|
},
|
|
@@ -74,6 +88,14 @@ const WebSocketChannel = {
|
|
|
74
88
|
mentions: true,
|
|
75
89
|
},
|
|
76
90
|
},
|
|
91
|
+
// 让配对写入的 channels.rol-websocket-channel 变更被判为 hot:
|
|
92
|
+
// OpenClaw 只热重载本 channel(进程内 stop→start,加载新 MQTT 连接),
|
|
93
|
+
// 不整机重启、不等 health-monitor。配置写完即在 gateway 进程内生效。
|
|
94
|
+
// (plugins.entries.rol-websocket-channel.* 已由 OpenClaw 内置的
|
|
95
|
+
// channel-plugin-state 规则自动判为 hot+restart-channel,无需在此声明。)
|
|
96
|
+
reload: {
|
|
97
|
+
configPrefixes: ["channels.rol-websocket-channel"],
|
|
98
|
+
},
|
|
77
99
|
configSchema: {
|
|
78
100
|
schema: {
|
|
79
101
|
type: "object",
|
|
@@ -466,6 +488,19 @@ async function handleIncomingMessage(payload, account, cfg, runtime, log, mqttTo
|
|
|
466
488
|
await handleCustomMessageType(msgType, innerData, traceId, account.accountId, mqttTopic);
|
|
467
489
|
return;
|
|
468
490
|
}
|
|
491
|
+
// 收到 sender 消息时,先回复一个确认
|
|
492
|
+
publishCustomMessageResponse({
|
|
493
|
+
type: "receiver",
|
|
494
|
+
trace_id: traceId,
|
|
495
|
+
source: "system",
|
|
496
|
+
timestamp: Date.now(),
|
|
497
|
+
success: true,
|
|
498
|
+
data: {
|
|
499
|
+
ok: true,
|
|
500
|
+
status: "received",
|
|
501
|
+
message: "已收到消息"
|
|
502
|
+
}
|
|
503
|
+
}, innerData, mqttTopic);
|
|
469
504
|
// 构造标准消息格式
|
|
470
505
|
const attachments = (innerData.attachments || []).map((att) => ({
|
|
471
506
|
url: att.url,
|
|
@@ -545,9 +580,9 @@ async function handleIncomingMessage(payload, account, cfg, runtime, log, mqttTo
|
|
|
545
580
|
let targetTopic = mqttTopic;
|
|
546
581
|
const sourceType = innerData?.source_type;
|
|
547
582
|
if (targetTopic.endsWith("#")) {
|
|
548
|
-
|
|
549
|
-
targetTopic = targetTopic.slice(0, -1) + replacement;
|
|
583
|
+
targetTopic = targetTopic.slice(0, -1) + 'app';
|
|
550
584
|
}
|
|
585
|
+
targetTopic = targetTopic.replace('device', 'app').replace('bot', 'app');
|
|
551
586
|
conn.ws.publish(targetTopic, JSON.stringify(replyMessage));
|
|
552
587
|
},
|
|
553
588
|
onError: (err) => {
|
|
@@ -689,9 +724,9 @@ function publishCustomMessageResponse(response, innerData, mqttTopic) {
|
|
|
689
724
|
let targetTopic = mqttTopic;
|
|
690
725
|
const sourceType = innerData?.source_type;
|
|
691
726
|
if (targetTopic.endsWith("#")) {
|
|
692
|
-
|
|
693
|
-
targetTopic = targetTopic.slice(0, -1) + replacement;
|
|
727
|
+
targetTopic = targetTopic.slice(0, -1) + 'app';
|
|
694
728
|
}
|
|
729
|
+
targetTopic = targetTopic.replace('device', 'app').replace('bot', 'app');
|
|
695
730
|
conn.ws.publish(targetTopic, JSON.stringify(response));
|
|
696
731
|
}
|
|
697
732
|
}
|
|
@@ -779,7 +814,11 @@ function registerAdminBridgeCli(api) {
|
|
|
779
814
|
}
|
|
780
815
|
catch (error) {
|
|
781
816
|
process.exitCode = 1;
|
|
782
|
-
|
|
817
|
+
// Emit the structured error on stdout (same channel as the
|
|
818
|
+
// success payload) so callers that only capture stdout still
|
|
819
|
+
// receive the full diagnostic — message, code, status,
|
|
820
|
+
// serviceMessage — instead of just exit code 1.
|
|
821
|
+
process.stdout.write(JSON.stringify(formatCliErrorPayload(error), null, 2) + "\n");
|
|
783
822
|
}
|
|
784
823
|
});
|
|
785
824
|
const mem9 = root
|
|
@@ -9,38 +9,38 @@ export function getProjectRoot() {
|
|
|
9
9
|
if (path.basename(root) === 'dist') {
|
|
10
10
|
root = path.dirname(root);
|
|
11
11
|
}
|
|
12
|
-
console.
|
|
13
|
-
console.
|
|
12
|
+
console.error('[paths] __dirname:', __dirname);
|
|
13
|
+
console.error('[paths] getProjectRoot:', root);
|
|
14
14
|
return root;
|
|
15
15
|
}
|
|
16
16
|
export function getOpenClawRoot() {
|
|
17
17
|
if (process.env.OPENCLAW_HOME) {
|
|
18
|
-
console.
|
|
18
|
+
console.error('[paths] getOpenClawRoot from env:', process.env.OPENCLAW_HOME);
|
|
19
19
|
return path.resolve(process.env.OPENCLAW_HOME);
|
|
20
20
|
}
|
|
21
21
|
const projectRoot = getProjectRoot();
|
|
22
22
|
const configuredRoot = findNearestOpenClawConfigRoot(projectRoot);
|
|
23
23
|
if (configuredRoot) {
|
|
24
|
-
console.
|
|
24
|
+
console.error('[paths] getOpenClawRoot from discovered openclaw.json:', configuredRoot);
|
|
25
25
|
return configuredRoot;
|
|
26
26
|
}
|
|
27
27
|
const openclawDir = findAncestorNamed(projectRoot, '.openclaw');
|
|
28
28
|
if (openclawDir) {
|
|
29
|
-
console.
|
|
29
|
+
console.error('[paths] getOpenClawRoot from .openclaw ancestor:', openclawDir);
|
|
30
30
|
return openclawDir;
|
|
31
31
|
}
|
|
32
32
|
const parentDir = path.dirname(projectRoot);
|
|
33
33
|
const parentName = path.basename(parentDir);
|
|
34
|
-
console.
|
|
35
|
-
console.
|
|
36
|
-
console.
|
|
34
|
+
console.error('[paths] projectRoot:', projectRoot);
|
|
35
|
+
console.error('[paths] parentDir:', parentDir);
|
|
36
|
+
console.error('[paths] parentName:', parentName);
|
|
37
37
|
if (parentName === 'extensions') {
|
|
38
38
|
const openclawRoot = path.dirname(parentDir);
|
|
39
|
-
console.
|
|
39
|
+
console.error('[paths] getOpenClawRoot (extensions):', openclawRoot);
|
|
40
40
|
return openclawRoot;
|
|
41
41
|
}
|
|
42
42
|
const fallbackRoot = path.resolve(projectRoot, '..');
|
|
43
|
-
console.
|
|
43
|
+
console.error('[paths] getOpenClawRoot (fallback):', fallbackRoot);
|
|
44
44
|
return fallbackRoot;
|
|
45
45
|
}
|
|
46
46
|
function findNearestOpenClawConfigRoot(startDir) {
|
|
@@ -1,14 +1,9 @@
|
|
|
1
|
-
import { exec, execFile } from 'node:child_process';
|
|
2
1
|
import path from 'node:path';
|
|
3
|
-
import { promisify } from 'node:util';
|
|
4
2
|
import { pathExists, readJsonFile, writeJsonFile } from '../lib/fs.js';
|
|
5
3
|
import { JsonRpcException, JSON_RPC_ERRORS } from '../jsonrpc.js';
|
|
6
|
-
const execFileAsync = promisify(execFile);
|
|
7
|
-
const execAsync = promisify(exec);
|
|
8
4
|
const DEFAULT_PLUGIN_ID = 'rol-websocket-channel';
|
|
9
5
|
const DEFAULT_PRODUCTION_BASE_URL = 'https://api.deotaland.ai';
|
|
10
6
|
const PAIR_ENDPOINT_PATH = '/api-core-bot/front/agent/agent/key/query';
|
|
11
|
-
const GATEWAY_SERVICE = 'openclaw-gateway.service';
|
|
12
7
|
export async function pairWithKey(options, context) {
|
|
13
8
|
const key = options.key.trim();
|
|
14
9
|
if (!key) {
|
|
@@ -21,7 +16,6 @@ export async function pairWithKey(options, context) {
|
|
|
21
16
|
const payload = await exchangePairKey(key, endpoint, options.auth, existingMqttUrl);
|
|
22
17
|
applyPairingConfig(config, key, payload);
|
|
23
18
|
await writeJsonFile(configPath, config);
|
|
24
|
-
const restart = await restartGateway(context.projectRoot);
|
|
25
19
|
return {
|
|
26
20
|
ok: true,
|
|
27
21
|
openclawRoot: context.openclawRoot,
|
|
@@ -34,7 +28,18 @@ export async function pairWithKey(options, context) {
|
|
|
34
28
|
`channels.${payload.pluginId}`
|
|
35
29
|
],
|
|
36
30
|
channel: payload.channel,
|
|
37
|
-
restart
|
|
31
|
+
// No gateway restart is dispatched here on purpose. Writing openclaw.json is
|
|
32
|
+
// enough: OpenClaw's config watcher detects the `channels` change, plans a
|
|
33
|
+
// gateway restart, and defers it until in-flight operations/replies/embedded
|
|
34
|
+
// runs drain before restarting with the new config. Spawning our own
|
|
35
|
+
// `openclaw gateway restart` would restart immediately and jump ahead of that
|
|
36
|
+
// deferral, killing the gateway mid-operation (the original pair-hang bug).
|
|
37
|
+
reload: {
|
|
38
|
+
dispatched: false,
|
|
39
|
+
handledBy: 'openclaw-config-watcher',
|
|
40
|
+
mode: 'deferred-until-idle',
|
|
41
|
+
note: 'OpenClaw restarts the gateway automatically after the config write.'
|
|
42
|
+
}
|
|
38
43
|
};
|
|
39
44
|
}
|
|
40
45
|
async function loadConfig(configPath) {
|
|
@@ -51,11 +56,21 @@ async function exchangePairKey(key, endpoint, authOverride, existingMqttUrl) {
|
|
|
51
56
|
if (auth) {
|
|
52
57
|
headers.Authorization = auth;
|
|
53
58
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
+
let response;
|
|
60
|
+
try {
|
|
61
|
+
response = await fetch(endpoint, {
|
|
62
|
+
method: 'POST',
|
|
63
|
+
headers,
|
|
64
|
+
body: JSON.stringify({ key })
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
throw new JsonRpcException(JSON_RPC_ERRORS.internalError, `pair exchange request failed: ${error instanceof Error ? error.message : String(error)}`, {
|
|
69
|
+
code: 'PAIR_EXCHANGE_REQUEST_FAILED',
|
|
70
|
+
endpoint,
|
|
71
|
+
cause: describeErrorCause(error)
|
|
72
|
+
});
|
|
73
|
+
}
|
|
59
74
|
const rawText = await response.text();
|
|
60
75
|
const payload = tryParseJson(rawText);
|
|
61
76
|
if (!response.ok) {
|
|
@@ -66,8 +81,23 @@ async function exchangePairKey(key, endpoint, authOverride, existingMqttUrl) {
|
|
|
66
81
|
payload
|
|
67
82
|
});
|
|
68
83
|
}
|
|
84
|
+
ensurePairExchangeSucceeded(payload, endpoint, response.status);
|
|
69
85
|
return normalizePairingPayload(payload, endpoint, existingMqttUrl);
|
|
70
86
|
}
|
|
87
|
+
function ensurePairExchangeSucceeded(raw, endpoint, status) {
|
|
88
|
+
if (!isRecord(raw) || raw.success !== false) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const serviceMessage = pickString(raw.message) ?? pickString(raw.msg);
|
|
92
|
+
throw new JsonRpcException(JSON_RPC_ERRORS.internalError, `pair exchange failed${serviceMessage ? `: ${serviceMessage}` : ''}`, {
|
|
93
|
+
code: 'PAIR_EXCHANGE_FAILED',
|
|
94
|
+
endpoint,
|
|
95
|
+
status,
|
|
96
|
+
serviceCode: raw.code,
|
|
97
|
+
success: false,
|
|
98
|
+
...(serviceMessage ? { serviceMessage } : {})
|
|
99
|
+
});
|
|
100
|
+
}
|
|
71
101
|
function normalizePairingPayload(raw, endpoint, existingMqttUrl) {
|
|
72
102
|
const root = unwrapPayload(raw);
|
|
73
103
|
const pluginId = pickString(root.pluginId) ?? pickString(root.plugin_id) ?? DEFAULT_PLUGIN_ID;
|
|
@@ -267,56 +297,6 @@ function resolveExistingMqttUrl(config) {
|
|
|
267
297
|
}
|
|
268
298
|
return pickString(channelConfig.mqttUrl);
|
|
269
299
|
}
|
|
270
|
-
async function restartGateway(cwd) {
|
|
271
|
-
const attempts = [];
|
|
272
|
-
try {
|
|
273
|
-
const { stdout, stderr } = await execAsync('openclaw gateway restart', { cwd });
|
|
274
|
-
return {
|
|
275
|
-
attempted: true,
|
|
276
|
-
success: true,
|
|
277
|
-
command: 'openclaw gateway restart',
|
|
278
|
-
stdout: stdout.trim(),
|
|
279
|
-
stderr: stderr.trim()
|
|
280
|
-
};
|
|
281
|
-
}
|
|
282
|
-
catch (error) {
|
|
283
|
-
attempts.push(formatRestartAttempt('openclaw gateway restart', error));
|
|
284
|
-
}
|
|
285
|
-
if (process.platform === 'win32') {
|
|
286
|
-
return {
|
|
287
|
-
attempted: true,
|
|
288
|
-
success: false,
|
|
289
|
-
message: 'openclaw gateway restart failed',
|
|
290
|
-
attempts
|
|
291
|
-
};
|
|
292
|
-
}
|
|
293
|
-
try {
|
|
294
|
-
await execFileAsync('systemctl', ['--user', 'restart', GATEWAY_SERVICE], { cwd });
|
|
295
|
-
return {
|
|
296
|
-
attempted: true,
|
|
297
|
-
success: true,
|
|
298
|
-
command: `systemctl --user restart ${GATEWAY_SERVICE}`,
|
|
299
|
-
attempts
|
|
300
|
-
};
|
|
301
|
-
}
|
|
302
|
-
catch (error) {
|
|
303
|
-
attempts.push(formatRestartAttempt(`systemctl --user restart ${GATEWAY_SERVICE}`, error));
|
|
304
|
-
return {
|
|
305
|
-
attempted: true,
|
|
306
|
-
success: false,
|
|
307
|
-
message: 'gateway restart failed',
|
|
308
|
-
attempts
|
|
309
|
-
};
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
function formatRestartAttempt(command, error) {
|
|
313
|
-
return {
|
|
314
|
-
command,
|
|
315
|
-
message: error instanceof Error ? error.message : String(error),
|
|
316
|
-
stdout: typeof error?.stdout === 'string' ? error.stdout.trim() : '',
|
|
317
|
-
stderr: typeof error?.stderr === 'string' ? error.stderr.trim() : ''
|
|
318
|
-
};
|
|
319
|
-
}
|
|
320
300
|
function normalizeGroupPolicy(value) {
|
|
321
301
|
if (value === 'pairing' || value === 'allowlist' || value === 'open' || value === 'disabled') {
|
|
322
302
|
return value;
|
|
@@ -341,6 +321,30 @@ function pickRecord(...values) {
|
|
|
341
321
|
function isRecord(value) {
|
|
342
322
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
343
323
|
}
|
|
324
|
+
function describeErrorCause(error) {
|
|
325
|
+
const detail = {};
|
|
326
|
+
if (error instanceof Error) {
|
|
327
|
+
detail.name = error.name;
|
|
328
|
+
detail.message = error.message;
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
detail.message = String(error);
|
|
332
|
+
}
|
|
333
|
+
const cause = error?.cause;
|
|
334
|
+
if (isRecord(cause)) {
|
|
335
|
+
const causeDetail = {};
|
|
336
|
+
for (const key of ['name', 'message', 'code', 'errno', 'syscall', 'hostname', 'address', 'port']) {
|
|
337
|
+
const value = cause[key];
|
|
338
|
+
if (['string', 'number', 'boolean'].includes(typeof value)) {
|
|
339
|
+
causeDetail[key] = value;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (Object.keys(causeDetail).length > 0) {
|
|
343
|
+
detail.cause = causeDetail;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return detail;
|
|
347
|
+
}
|
|
344
348
|
function throwPairingError(code, message, debug) {
|
|
345
349
|
throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, message, {
|
|
346
350
|
code,
|
|
@@ -12,9 +12,10 @@ export const getSession = async (params, context) => {
|
|
|
12
12
|
: undefined;
|
|
13
13
|
const sessionId = typeof objectParams.sessionId === 'string' ? objectParams.sessionId : null;
|
|
14
14
|
const requestedLimit = typeof objectParams.limit === 'number' ? objectParams.limit : MAX_SESSION_MESSAGES;
|
|
15
|
-
const
|
|
15
|
+
const beforeId = typeof objectParams.beforeId === 'string' && objectParams.beforeId.trim()
|
|
16
|
+
? objectParams.beforeId.trim()
|
|
17
|
+
: undefined;
|
|
16
18
|
const limit = normalizePageSize(requestedLimit, MAX_SESSION_MESSAGES);
|
|
17
|
-
const offset = normalizeOffset(requestedOffset);
|
|
18
19
|
if (!sessionId) {
|
|
19
20
|
throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, 'Missing required parameter: sessionId');
|
|
20
21
|
}
|
|
@@ -23,7 +24,7 @@ export const getSession = async (params, context) => {
|
|
|
23
24
|
throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, agentId ? `Session not found: ${sessionId} for agent ${agentId}` : `Session not found: ${sessionId}`);
|
|
24
25
|
}
|
|
25
26
|
const sessionFile = session.sessionFilePath ?? path.join(context.openclawRoot, 'agents', session.agentName, 'sessions', `${session.sessionId ?? sessionId}.jsonl`);
|
|
26
|
-
const messages = await readSessionMessages(sessionFile, limit,
|
|
27
|
+
const messages = await readSessionMessages(sessionFile, limit, beforeId);
|
|
27
28
|
return {
|
|
28
29
|
agentId: session.agentId,
|
|
29
30
|
agentName: session.agentName,
|
|
@@ -39,7 +40,8 @@ export const getSession = async (params, context) => {
|
|
|
39
40
|
messages: {
|
|
40
41
|
total: messages.total,
|
|
41
42
|
limit,
|
|
42
|
-
|
|
43
|
+
hasMore: messages.hasMore,
|
|
44
|
+
nextBeforeId: messages.nextBeforeId,
|
|
43
45
|
items: messages.items
|
|
44
46
|
}
|
|
45
47
|
};
|
|
@@ -131,15 +133,9 @@ function normalizePageSize(value, max) {
|
|
|
131
133
|
}
|
|
132
134
|
return Math.min(normalized, max);
|
|
133
135
|
}
|
|
134
|
-
function
|
|
135
|
-
if (!Number.isFinite(value)) {
|
|
136
|
-
return 0;
|
|
137
|
-
}
|
|
138
|
-
return Math.max(0, Math.floor(value));
|
|
139
|
-
}
|
|
140
|
-
async function readSessionMessages(filePath, limit, offset) {
|
|
136
|
+
async function readSessionMessages(filePath, limit, beforeId) {
|
|
141
137
|
if (!(await pathExists(filePath))) {
|
|
142
|
-
return { total: 0, items: [] };
|
|
138
|
+
return { total: 0, hasMore: false, nextBeforeId: null, items: [] };
|
|
143
139
|
}
|
|
144
140
|
const messages = [];
|
|
145
141
|
const fileStream = fs.createReadStream(filePath);
|
|
@@ -158,9 +154,19 @@ async function readSessionMessages(filePath, limit, offset) {
|
|
|
158
154
|
continue;
|
|
159
155
|
}
|
|
160
156
|
}
|
|
157
|
+
const endExclusive = beforeId ? messages.findIndex((message) => message.id === beforeId) : messages.length;
|
|
158
|
+
if (endExclusive < 0) {
|
|
159
|
+
throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, `beforeId not found: ${beforeId}`);
|
|
160
|
+
}
|
|
161
|
+
const start = Math.max(0, endExclusive - limit);
|
|
162
|
+
const items = messages.slice(start, endExclusive);
|
|
163
|
+
const hasMore = start > 0;
|
|
164
|
+
const firstItemId = typeof items[0]?.id === 'string' ? items[0].id : null;
|
|
161
165
|
return {
|
|
162
166
|
total: messages.length,
|
|
163
|
-
|
|
167
|
+
hasMore,
|
|
168
|
+
nextBeforeId: hasMore ? firstItemId : null,
|
|
169
|
+
items
|
|
164
170
|
};
|
|
165
171
|
}
|
|
166
172
|
async function findInstalledSkill(context, skillSlug) {
|
|
@@ -9,6 +9,7 @@ const execFileAsync = promisify(execFile);
|
|
|
9
9
|
const UPDATE_COMMAND_TIMEOUT_MS = 10 * 60 * 1000;
|
|
10
10
|
const UPDATE_COMMAND_MAX_BUFFER = 10 * 1024 * 1024;
|
|
11
11
|
const CHANNEL_FALLBACK_VERSION = '1.5.9';
|
|
12
|
+
const MIN_OPENCLAW_PLUGIN_UPDATE_VERSION = '2026.5.6';
|
|
12
13
|
export const ping = async () => {
|
|
13
14
|
return {
|
|
14
15
|
ok: true,
|
|
@@ -74,6 +75,18 @@ export const doctorFix = async (_params, context) => {
|
|
|
74
75
|
}
|
|
75
76
|
};
|
|
76
77
|
export const pluginSelfUpdate = async (_params, context) => {
|
|
78
|
+
const versionCheck = await checkOpenClawVersionForPluginUpdate(context);
|
|
79
|
+
if (!versionCheck.supported) {
|
|
80
|
+
return {
|
|
81
|
+
ok: false,
|
|
82
|
+
action: 'pluginSelfUpdate',
|
|
83
|
+
plugin: 'rol-websocket-channel',
|
|
84
|
+
skipped: true,
|
|
85
|
+
reason: versionCheck.reason,
|
|
86
|
+
message: versionCheck.message,
|
|
87
|
+
restartRecommended: false
|
|
88
|
+
};
|
|
89
|
+
}
|
|
77
90
|
const result = await runOpenClawCommand(['plugins', 'update', 'rol-websocket-channel'], context, 'pluginSelfUpdate');
|
|
78
91
|
const output = `${result.stdout}\n${result.stderr}`;
|
|
79
92
|
if (isPathSourceUpdateSkip(output)) {
|
|
@@ -96,6 +109,45 @@ export const pluginSelfUpdate = async (_params, context) => {
|
|
|
96
109
|
...result
|
|
97
110
|
};
|
|
98
111
|
};
|
|
112
|
+
async function checkOpenClawVersionForPluginUpdate(context) {
|
|
113
|
+
try {
|
|
114
|
+
const result = await runOpenClawCommand(['--version'], context, 'pluginSelfUpdate.versionCheck');
|
|
115
|
+
const output = `${result.stdout}\n${result.stderr}`.trim();
|
|
116
|
+
const currentVersion = extractOpenClawVersion(output);
|
|
117
|
+
if (!currentVersion) {
|
|
118
|
+
return {
|
|
119
|
+
supported: false,
|
|
120
|
+
reason: 'openclaw-version-unreadable',
|
|
121
|
+
message: `Unable to detect OpenClaw version. Please install or upgrade OpenClaw to ${MIN_OPENCLAW_PLUGIN_UPDATE_VERSION} or newer before updating rol-websocket-channel.`,
|
|
122
|
+
currentVersion: null,
|
|
123
|
+
output
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
if (!isOpenClawVersionAtLeast(currentVersion, MIN_OPENCLAW_PLUGIN_UPDATE_VERSION)) {
|
|
127
|
+
return {
|
|
128
|
+
supported: false,
|
|
129
|
+
reason: 'openclaw-version-too-old',
|
|
130
|
+
message: `OpenClaw ${currentVersion} is too old for rol-websocket-channel self update. Please install or upgrade OpenClaw to ${MIN_OPENCLAW_PLUGIN_UPDATE_VERSION} or newer.`,
|
|
131
|
+
currentVersion,
|
|
132
|
+
output
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
supported: true,
|
|
137
|
+
currentVersion,
|
|
138
|
+
output
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
return {
|
|
143
|
+
supported: false,
|
|
144
|
+
reason: 'openclaw-version-check-failed',
|
|
145
|
+
message: `Unable to check OpenClaw version. Please install or upgrade OpenClaw to ${MIN_OPENCLAW_PLUGIN_UPDATE_VERSION} or newer before updating rol-websocket-channel.`,
|
|
146
|
+
currentVersion: null,
|
|
147
|
+
output: error instanceof Error ? error.message : String(error)
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
}
|
|
99
151
|
export const currentVersion = async (_params, context) => {
|
|
100
152
|
const [channelPackage, registryInfo] = await Promise.all([
|
|
101
153
|
readJsonFile(path.join(context.projectRoot, 'package.json')),
|
|
@@ -117,6 +169,23 @@ export const currentVersion = async (_params, context) => {
|
|
|
117
169
|
};
|
|
118
170
|
};
|
|
119
171
|
export const logs = async (params, context) => {
|
|
172
|
+
const limit = 10;
|
|
173
|
+
const maxBytes = normalizePositiveInteger(params?.maxBytes, 250000);
|
|
174
|
+
try {
|
|
175
|
+
const result = await runOpenClawCommand(['logs', '--json', '--limit', String(limit), '--max-bytes', String(maxBytes)], context, 'logs');
|
|
176
|
+
return {
|
|
177
|
+
ok: true,
|
|
178
|
+
source: 'openclaw logs',
|
|
179
|
+
nextOffset: null,
|
|
180
|
+
lines: parseOpenClawLogOutput(result.stdout).slice(-limit).reverse()
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
catch (officialError) {
|
|
184
|
+
console.error(`[system] openclaw logs failed, falling back to local file scan: ${officialError instanceof Error ? officialError.message : String(officialError)}`);
|
|
185
|
+
return await readLocalLogFiles(context, limit, maxBytes);
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
async function readLocalLogFiles(context, limit, maxBytes) {
|
|
120
189
|
try {
|
|
121
190
|
// 根据实际情况可能需要调整,这里默认尝试 project root 或 user home 下的 .openclaw/logs
|
|
122
191
|
// 很多时候全局日志位于 ~/.openclaw/logs/gateway.log 或工程目录下的 .openclaw 文件夹中
|
|
@@ -168,8 +237,6 @@ export const logs = async (params, context) => {
|
|
|
168
237
|
if (logFiles.length === 0) {
|
|
169
238
|
return { ok: false, error: `No .log files found in directory: ${logDir}` };
|
|
170
239
|
}
|
|
171
|
-
const limit = 10;
|
|
172
|
-
const maxBytes = params?.maxBytes ?? 250000;
|
|
173
240
|
let offset = undefined;
|
|
174
241
|
// 获取所有候选日志的详细信息并排序(对应 ls -t)
|
|
175
242
|
const fileStats = await Promise.all(logFiles.map(async (file) => {
|
|
@@ -226,7 +293,31 @@ export const logs = async (params, context) => {
|
|
|
226
293
|
error: error instanceof Error ? error.message : String(error)
|
|
227
294
|
};
|
|
228
295
|
}
|
|
229
|
-
}
|
|
296
|
+
}
|
|
297
|
+
function parseOpenClawLogOutput(output) {
|
|
298
|
+
return output
|
|
299
|
+
.split(/\r?\n/)
|
|
300
|
+
.map((line) => line.trim())
|
|
301
|
+
.filter((line) => line.length > 0)
|
|
302
|
+
.map((line) => {
|
|
303
|
+
try {
|
|
304
|
+
return JSON.parse(line);
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
return line;
|
|
308
|
+
}
|
|
309
|
+
})
|
|
310
|
+
.filter((entry) => !(isJsonObject(entry) && entry.type === 'meta'));
|
|
311
|
+
}
|
|
312
|
+
function normalizePositiveInteger(value, fallback) {
|
|
313
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
|
|
314
|
+
return fallback;
|
|
315
|
+
}
|
|
316
|
+
return Math.floor(value);
|
|
317
|
+
}
|
|
318
|
+
function isJsonObject(value) {
|
|
319
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
320
|
+
}
|
|
230
321
|
async function runOpenClawCommand(args, context, action) {
|
|
231
322
|
const command = process.env.OPENCLAW_BIN || 'openclaw';
|
|
232
323
|
const options = buildExecOptions(context.openclawRoot, context.openclawRoot);
|
|
@@ -339,6 +430,28 @@ function normalizeVersion(value) {
|
|
|
339
430
|
}
|
|
340
431
|
return value.trim().replace(/^v/i, '');
|
|
341
432
|
}
|
|
433
|
+
function isOpenClawVersionAtLeast(current, minimum) {
|
|
434
|
+
const currentVersion = extractOpenClawVersion(current);
|
|
435
|
+
const minimumVersion = extractOpenClawVersion(minimum);
|
|
436
|
+
if (!currentVersion || !minimumVersion) {
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
const currentParts = currentVersion.split('.').map(Number);
|
|
440
|
+
const minimumParts = minimumVersion.split('.').map(Number);
|
|
441
|
+
for (let i = 0; i < Math.max(currentParts.length, minimumParts.length); i += 1) {
|
|
442
|
+
const currentPart = currentParts[i] ?? 0;
|
|
443
|
+
const minimumPart = minimumParts[i] ?? 0;
|
|
444
|
+
if (currentPart > minimumPart)
|
|
445
|
+
return true;
|
|
446
|
+
if (currentPart < minimumPart)
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
return true;
|
|
450
|
+
}
|
|
451
|
+
function extractOpenClawVersion(value) {
|
|
452
|
+
const match = value.match(/\b(\d{4}\.\d+\.\d+)\b/);
|
|
453
|
+
return match ? match[1] : null;
|
|
454
|
+
}
|
|
342
455
|
function isPathSourceUpdateSkip(output) {
|
|
343
456
|
return /Skipping\s+"?rol-websocket-channel"?\s+\(source:\s*path\)/i.test(output);
|
|
344
457
|
}
|