rol-websocket-channel 1.7.3 → 1.7.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.
|
@@ -51,11 +51,21 @@ async function exchangePairKey(key, endpoint, authOverride, existingMqttUrl) {
|
|
|
51
51
|
if (auth) {
|
|
52
52
|
headers.Authorization = auth;
|
|
53
53
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
54
|
+
let response;
|
|
55
|
+
try {
|
|
56
|
+
response = await fetch(endpoint, {
|
|
57
|
+
method: 'POST',
|
|
58
|
+
headers,
|
|
59
|
+
body: JSON.stringify({ key })
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
throw new JsonRpcException(JSON_RPC_ERRORS.internalError, `pair exchange request failed: ${error instanceof Error ? error.message : String(error)}`, {
|
|
64
|
+
code: 'PAIR_EXCHANGE_REQUEST_FAILED',
|
|
65
|
+
endpoint,
|
|
66
|
+
cause: describeErrorCause(error)
|
|
67
|
+
});
|
|
68
|
+
}
|
|
59
69
|
const rawText = await response.text();
|
|
60
70
|
const payload = tryParseJson(rawText);
|
|
61
71
|
if (!response.ok) {
|
|
@@ -66,8 +76,23 @@ async function exchangePairKey(key, endpoint, authOverride, existingMqttUrl) {
|
|
|
66
76
|
payload
|
|
67
77
|
});
|
|
68
78
|
}
|
|
79
|
+
ensurePairExchangeSucceeded(payload, endpoint, response.status);
|
|
69
80
|
return normalizePairingPayload(payload, endpoint, existingMqttUrl);
|
|
70
81
|
}
|
|
82
|
+
function ensurePairExchangeSucceeded(raw, endpoint, status) {
|
|
83
|
+
if (!isRecord(raw) || raw.success !== false) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const serviceMessage = pickString(raw.message) ?? pickString(raw.msg);
|
|
87
|
+
throw new JsonRpcException(JSON_RPC_ERRORS.internalError, `pair exchange failed${serviceMessage ? `: ${serviceMessage}` : ''}`, {
|
|
88
|
+
code: 'PAIR_EXCHANGE_FAILED',
|
|
89
|
+
endpoint,
|
|
90
|
+
status,
|
|
91
|
+
serviceCode: raw.code,
|
|
92
|
+
success: false,
|
|
93
|
+
...(serviceMessage ? { serviceMessage } : {})
|
|
94
|
+
});
|
|
95
|
+
}
|
|
71
96
|
function normalizePairingPayload(raw, endpoint, existingMqttUrl) {
|
|
72
97
|
const root = unwrapPayload(raw);
|
|
73
98
|
const pluginId = pickString(root.pluginId) ?? pickString(root.plugin_id) ?? DEFAULT_PLUGIN_ID;
|
|
@@ -341,6 +366,30 @@ function pickRecord(...values) {
|
|
|
341
366
|
function isRecord(value) {
|
|
342
367
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
343
368
|
}
|
|
369
|
+
function describeErrorCause(error) {
|
|
370
|
+
const detail = {};
|
|
371
|
+
if (error instanceof Error) {
|
|
372
|
+
detail.name = error.name;
|
|
373
|
+
detail.message = error.message;
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
detail.message = String(error);
|
|
377
|
+
}
|
|
378
|
+
const cause = error?.cause;
|
|
379
|
+
if (isRecord(cause)) {
|
|
380
|
+
const causeDetail = {};
|
|
381
|
+
for (const key of ['name', 'message', 'code', 'errno', 'syscall', 'hostname', 'address', 'port']) {
|
|
382
|
+
const value = cause[key];
|
|
383
|
+
if (['string', 'number', 'boolean'].includes(typeof value)) {
|
|
384
|
+
causeDetail[key] = value;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
if (Object.keys(causeDetail).length > 0) {
|
|
388
|
+
detail.cause = causeDetail;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return detail;
|
|
392
|
+
}
|
|
344
393
|
function throwPairingError(code, message, debug) {
|
|
345
394
|
throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, message, {
|
|
346
395
|
code,
|
|
@@ -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
|
}
|
package/package.json
CHANGED
|
@@ -114,11 +114,24 @@ async function exchangePairKey(
|
|
|
114
114
|
headers.Authorization = auth;
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
117
|
+
let response: Response;
|
|
118
|
+
try {
|
|
119
|
+
response = await fetch(endpoint, {
|
|
120
|
+
method: 'POST',
|
|
121
|
+
headers,
|
|
122
|
+
body: JSON.stringify({ key })
|
|
123
|
+
});
|
|
124
|
+
} catch (error) {
|
|
125
|
+
throw new JsonRpcException(
|
|
126
|
+
JSON_RPC_ERRORS.internalError,
|
|
127
|
+
`pair exchange request failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
128
|
+
{
|
|
129
|
+
code: 'PAIR_EXCHANGE_REQUEST_FAILED',
|
|
130
|
+
endpoint,
|
|
131
|
+
cause: describeErrorCause(error)
|
|
132
|
+
}
|
|
133
|
+
);
|
|
134
|
+
}
|
|
122
135
|
|
|
123
136
|
const rawText = await response.text();
|
|
124
137
|
const payload = tryParseJson(rawText);
|
|
@@ -135,9 +148,31 @@ async function exchangePairKey(
|
|
|
135
148
|
);
|
|
136
149
|
}
|
|
137
150
|
|
|
151
|
+
ensurePairExchangeSucceeded(payload, endpoint, response.status);
|
|
152
|
+
|
|
138
153
|
return normalizePairingPayload(payload, endpoint, existingMqttUrl);
|
|
139
154
|
}
|
|
140
155
|
|
|
156
|
+
function ensurePairExchangeSucceeded(raw: unknown, endpoint: string, status: number): void {
|
|
157
|
+
if (!isRecord(raw) || raw.success !== false) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const serviceMessage = pickString(raw.message) ?? pickString(raw.msg);
|
|
162
|
+
throw new JsonRpcException(
|
|
163
|
+
JSON_RPC_ERRORS.internalError,
|
|
164
|
+
`pair exchange failed${serviceMessage ? `: ${serviceMessage}` : ''}`,
|
|
165
|
+
{
|
|
166
|
+
code: 'PAIR_EXCHANGE_FAILED',
|
|
167
|
+
endpoint,
|
|
168
|
+
status,
|
|
169
|
+
serviceCode: raw.code,
|
|
170
|
+
success: false,
|
|
171
|
+
...(serviceMessage ? { serviceMessage } : {})
|
|
172
|
+
}
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
141
176
|
function normalizePairingPayload(
|
|
142
177
|
raw: unknown,
|
|
143
178
|
endpoint: string,
|
|
@@ -448,6 +483,33 @@ function isRecord(value: unknown): value is Record<string, any> {
|
|
|
448
483
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
449
484
|
}
|
|
450
485
|
|
|
486
|
+
function describeErrorCause(error: unknown): Record<string, unknown> {
|
|
487
|
+
const detail: Record<string, unknown> = {};
|
|
488
|
+
if (error instanceof Error) {
|
|
489
|
+
detail.name = error.name;
|
|
490
|
+
detail.message = error.message;
|
|
491
|
+
} else {
|
|
492
|
+
detail.message = String(error);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const cause = (error as { cause?: unknown } | null | undefined)?.cause;
|
|
496
|
+
if (isRecord(cause)) {
|
|
497
|
+
const causeDetail: Record<string, unknown> = {};
|
|
498
|
+
for (const key of ['name', 'message', 'code', 'errno', 'syscall', 'hostname', 'address', 'port']) {
|
|
499
|
+
const value = cause[key];
|
|
500
|
+
if (['string', 'number', 'boolean'].includes(typeof value)) {
|
|
501
|
+
causeDetail[key] = value;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (Object.keys(causeDetail).length > 0) {
|
|
506
|
+
detail.cause = causeDetail;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
return detail;
|
|
511
|
+
}
|
|
512
|
+
|
|
451
513
|
function throwPairingError(code: string, message: string, debug?: PairingPayloadDebug): never {
|
|
452
514
|
throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, message, {
|
|
453
515
|
code,
|
|
@@ -11,6 +11,7 @@ const execFileAsync = promisify(execFile);
|
|
|
11
11
|
const UPDATE_COMMAND_TIMEOUT_MS = 10 * 60 * 1000;
|
|
12
12
|
const UPDATE_COMMAND_MAX_BUFFER = 10 * 1024 * 1024;
|
|
13
13
|
const CHANNEL_FALLBACK_VERSION = '1.5.9';
|
|
14
|
+
const MIN_OPENCLAW_PLUGIN_UPDATE_VERSION = '2026.5.6';
|
|
14
15
|
|
|
15
16
|
export const ping: MethodHandler = async (): Promise<JsonValue> => {
|
|
16
17
|
return {
|
|
@@ -81,6 +82,19 @@ export const doctorFix: MethodHandler = async (_params, context: MethodContext):
|
|
|
81
82
|
};
|
|
82
83
|
|
|
83
84
|
export const pluginSelfUpdate: MethodHandler = async (_params, context: MethodContext): Promise<JsonValue> => {
|
|
85
|
+
const versionCheck = await checkOpenClawVersionForPluginUpdate(context);
|
|
86
|
+
if (!versionCheck.supported) {
|
|
87
|
+
return {
|
|
88
|
+
ok: false,
|
|
89
|
+
action: 'pluginSelfUpdate',
|
|
90
|
+
plugin: 'rol-websocket-channel',
|
|
91
|
+
skipped: true,
|
|
92
|
+
reason: versionCheck.reason,
|
|
93
|
+
message: versionCheck.message,
|
|
94
|
+
restartRecommended: false
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
84
98
|
const result = await runOpenClawCommand(
|
|
85
99
|
['plugins', 'update', 'rol-websocket-channel'],
|
|
86
100
|
context,
|
|
@@ -110,6 +124,61 @@ export const pluginSelfUpdate: MethodHandler = async (_params, context: MethodCo
|
|
|
110
124
|
};
|
|
111
125
|
};
|
|
112
126
|
|
|
127
|
+
type PluginUpdateVersionCheck =
|
|
128
|
+
| {
|
|
129
|
+
supported: true;
|
|
130
|
+
currentVersion: string;
|
|
131
|
+
output: string;
|
|
132
|
+
}
|
|
133
|
+
| {
|
|
134
|
+
supported: false;
|
|
135
|
+
reason: string;
|
|
136
|
+
message: string;
|
|
137
|
+
currentVersion: string | null;
|
|
138
|
+
output: string;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
async function checkOpenClawVersionForPluginUpdate(context: MethodContext): Promise<PluginUpdateVersionCheck> {
|
|
142
|
+
try {
|
|
143
|
+
const result = await runOpenClawCommand(['--version'], context, 'pluginSelfUpdate.versionCheck');
|
|
144
|
+
const output = `${result.stdout}\n${result.stderr}`.trim();
|
|
145
|
+
const currentVersion = extractOpenClawVersion(output);
|
|
146
|
+
if (!currentVersion) {
|
|
147
|
+
return {
|
|
148
|
+
supported: false,
|
|
149
|
+
reason: 'openclaw-version-unreadable',
|
|
150
|
+
message: `Unable to detect OpenClaw version. Please install or upgrade OpenClaw to ${MIN_OPENCLAW_PLUGIN_UPDATE_VERSION} or newer before updating rol-websocket-channel.`,
|
|
151
|
+
currentVersion: null,
|
|
152
|
+
output
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (!isOpenClawVersionAtLeast(currentVersion, MIN_OPENCLAW_PLUGIN_UPDATE_VERSION)) {
|
|
157
|
+
return {
|
|
158
|
+
supported: false,
|
|
159
|
+
reason: 'openclaw-version-too-old',
|
|
160
|
+
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.`,
|
|
161
|
+
currentVersion,
|
|
162
|
+
output
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
supported: true,
|
|
168
|
+
currentVersion,
|
|
169
|
+
output
|
|
170
|
+
};
|
|
171
|
+
} catch (error) {
|
|
172
|
+
return {
|
|
173
|
+
supported: false,
|
|
174
|
+
reason: 'openclaw-version-check-failed',
|
|
175
|
+
message: `Unable to check OpenClaw version. Please install or upgrade OpenClaw to ${MIN_OPENCLAW_PLUGIN_UPDATE_VERSION} or newer before updating rol-websocket-channel.`,
|
|
176
|
+
currentVersion: null,
|
|
177
|
+
output: error instanceof Error ? error.message : String(error)
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
113
182
|
export const currentVersion: MethodHandler = async (_params, context: MethodContext): Promise<JsonValue> => {
|
|
114
183
|
const [channelPackage, registryInfo] = await Promise.all([
|
|
115
184
|
readJsonFile<{ name?: string; version?: string }>(path.join(context.projectRoot, 'package.json')),
|
|
@@ -140,6 +209,34 @@ export const currentVersion: MethodHandler = async (_params, context: MethodCont
|
|
|
140
209
|
};
|
|
141
210
|
|
|
142
211
|
export const logs: MethodHandler = async (params: any, context: MethodContext): Promise<JsonValue> => {
|
|
212
|
+
const limit = 10;
|
|
213
|
+
const maxBytes = normalizePositiveInteger(params?.maxBytes, 250000);
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
const result = await runOpenClawCommand(
|
|
217
|
+
['logs', '--json', '--limit', String(limit), '--max-bytes', String(maxBytes)],
|
|
218
|
+
context,
|
|
219
|
+
'logs'
|
|
220
|
+
);
|
|
221
|
+
return {
|
|
222
|
+
ok: true,
|
|
223
|
+
source: 'openclaw logs',
|
|
224
|
+
nextOffset: null,
|
|
225
|
+
lines: parseOpenClawLogOutput(result.stdout).slice(-limit).reverse()
|
|
226
|
+
};
|
|
227
|
+
} catch (officialError) {
|
|
228
|
+
console.error(
|
|
229
|
+
`[system] openclaw logs failed, falling back to local file scan: ${officialError instanceof Error ? officialError.message : String(officialError)}`
|
|
230
|
+
);
|
|
231
|
+
return await readLocalLogFiles(context, limit, maxBytes);
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
async function readLocalLogFiles(
|
|
236
|
+
context: MethodContext,
|
|
237
|
+
limit: number,
|
|
238
|
+
maxBytes: number
|
|
239
|
+
): Promise<JsonValue> {
|
|
143
240
|
try {
|
|
144
241
|
// 根据实际情况可能需要调整,这里默认尝试 project root 或 user home 下的 .openclaw/logs
|
|
145
242
|
// 很多时候全局日志位于 ~/.openclaw/logs/gateway.log 或工程目录下的 .openclaw 文件夹中
|
|
@@ -201,8 +298,6 @@ export const logs: MethodHandler = async (params: any, context: MethodContext):
|
|
|
201
298
|
return { ok: false, error: `No .log files found in directory: ${logDir}` };
|
|
202
299
|
}
|
|
203
300
|
|
|
204
|
-
const limit = 10;
|
|
205
|
-
const maxBytes = params?.maxBytes ?? 250000;
|
|
206
301
|
let offset: number | undefined = undefined;
|
|
207
302
|
|
|
208
303
|
// 获取所有候选日志的详细信息并排序(对应 ls -t)
|
|
@@ -267,7 +362,34 @@ export const logs: MethodHandler = async (params: any, context: MethodContext):
|
|
|
267
362
|
error: error instanceof Error ? error.message : String(error)
|
|
268
363
|
};
|
|
269
364
|
}
|
|
270
|
-
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function parseOpenClawLogOutput(output: string): JsonValue[] {
|
|
368
|
+
return output
|
|
369
|
+
.split(/\r?\n/)
|
|
370
|
+
.map((line) => line.trim())
|
|
371
|
+
.filter((line) => line.length > 0)
|
|
372
|
+
.map((line) => {
|
|
373
|
+
try {
|
|
374
|
+
return JSON.parse(line) as JsonValue;
|
|
375
|
+
} catch {
|
|
376
|
+
return line;
|
|
377
|
+
}
|
|
378
|
+
})
|
|
379
|
+
.filter((entry) => !(isJsonObject(entry) && entry.type === 'meta'));
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function normalizePositiveInteger(value: unknown, fallback: number): number {
|
|
383
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
|
|
384
|
+
return fallback;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return Math.floor(value);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function isJsonObject(value: JsonValue): value is { [key: string]: JsonValue } {
|
|
391
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
392
|
+
}
|
|
271
393
|
|
|
272
394
|
async function runOpenClawCommand(
|
|
273
395
|
args: string[],
|
|
@@ -432,6 +554,30 @@ function normalizeVersion(value: string | undefined): string {
|
|
|
432
554
|
return value.trim().replace(/^v/i, '');
|
|
433
555
|
}
|
|
434
556
|
|
|
557
|
+
function isOpenClawVersionAtLeast(current: string, minimum: string): boolean {
|
|
558
|
+
const currentVersion = extractOpenClawVersion(current);
|
|
559
|
+
const minimumVersion = extractOpenClawVersion(minimum);
|
|
560
|
+
if (!currentVersion || !minimumVersion) {
|
|
561
|
+
return false;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const currentParts = currentVersion.split('.').map(Number);
|
|
565
|
+
const minimumParts = minimumVersion.split('.').map(Number);
|
|
566
|
+
for (let i = 0; i < Math.max(currentParts.length, minimumParts.length); i += 1) {
|
|
567
|
+
const currentPart = currentParts[i] ?? 0;
|
|
568
|
+
const minimumPart = minimumParts[i] ?? 0;
|
|
569
|
+
if (currentPart > minimumPart) return true;
|
|
570
|
+
if (currentPart < minimumPart) return false;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
return true;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function extractOpenClawVersion(value: string): string | null {
|
|
577
|
+
const match = value.match(/\b(\d{4}\.\d+\.\d+)\b/);
|
|
578
|
+
return match ? match[1] : null;
|
|
579
|
+
}
|
|
580
|
+
|
|
435
581
|
function isPathSourceUpdateSkip(output: string): boolean {
|
|
436
582
|
return /Skipping\s+"?rol-websocket-channel"?\s+\(source:\s*path\)/i.test(output);
|
|
437
583
|
}
|