rol-websocket-channel 1.4.2 → 1.4.9
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 /346/226/260/345/242/236/346/226/207/344/273/266/345/212/237/350/203/275.md" → MQTT-API 5-6.md } +89 -1
- package/dist/index.js +6 -0
- package/dist/message-handler.js +13 -1
- package/dist/src/admin/cli-manifest.test.js +62 -0
- package/dist/src/admin/methods/artifacts.js +96 -2
- package/dist/src/admin/methods/artifacts.test.js +19 -0
- package/dist/src/admin/methods/index.js +3 -1
- package/dist/src/admin/methods/mem9.js +40 -1
- package/dist/src/admin/methods/mem9.test.js +34 -0
- package/dist/src/admin/methods/system.js +92 -1
- package/index.ts +6 -0
- package/message-handler.ts +15 -1
- package/openclaw.plugin.json +80 -0
- package/package.json +1 -1
- package/src/admin/cli-manifest.test.ts +67 -0
- package/src/admin/methods/artifacts.test.ts +35 -0
- package/src/admin/methods/artifacts.ts +140 -2
- package/src/admin/methods/index.ts +3 -1
- package/src/admin/methods/mem9.test.ts +39 -0
- package/src/admin/methods/mem9.ts +48 -1
- package/src/admin/methods/system.ts +129 -1
|
@@ -85,6 +85,71 @@
|
|
|
85
85
|
|
|
86
86
|
---
|
|
87
87
|
|
|
88
|
+
### openclawUpdate - 升级 OpenClaw
|
|
89
|
+
|
|
90
|
+
**说明**
|
|
91
|
+
- 固定执行 `openclaw update`。
|
|
92
|
+
- 用于 App 上的“升级 OpenClaw”按钮。
|
|
93
|
+
- 返回 `stdout` / `stderr`,失败时外层 `ok` 为 `false`。
|
|
94
|
+
|
|
95
|
+
**请求**
|
|
96
|
+
```json
|
|
97
|
+
{
|
|
98
|
+
"type": "openclawUpdate",
|
|
99
|
+
"trace_id": "openclaw-update-001",
|
|
100
|
+
"data": {}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**成功返回 data.result 示例**
|
|
105
|
+
```json
|
|
106
|
+
{
|
|
107
|
+
"ok": true,
|
|
108
|
+
"action": "openclawUpdate",
|
|
109
|
+
"restartRecommended": true,
|
|
110
|
+
"command": "openclaw",
|
|
111
|
+
"args": ["update"],
|
|
112
|
+
"cwd": "/home/user/.openclaw",
|
|
113
|
+
"stdout": "",
|
|
114
|
+
"stderr": ""
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
### pluginSelfUpdate - 升级 rol-websocket-channel 插件
|
|
121
|
+
|
|
122
|
+
**说明**
|
|
123
|
+
- 固定执行 `openclaw plugins install clawhub:rol-websocket-channel`。
|
|
124
|
+
- 用于 App 上的“升级插件”按钮。
|
|
125
|
+
- 命令执行成功后,新的插件代码通常需要 OpenClaw/插件进程重新加载后才会生效。
|
|
126
|
+
|
|
127
|
+
**请求**
|
|
128
|
+
```json
|
|
129
|
+
{
|
|
130
|
+
"type": "pluginSelfUpdate",
|
|
131
|
+
"trace_id": "plugin-self-update-001",
|
|
132
|
+
"data": {}
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**成功返回 data.result 示例**
|
|
137
|
+
```json
|
|
138
|
+
{
|
|
139
|
+
"ok": true,
|
|
140
|
+
"action": "pluginSelfUpdate",
|
|
141
|
+
"plugin": "clawhub:rol-websocket-channel",
|
|
142
|
+
"restartRecommended": true,
|
|
143
|
+
"command": "openclaw",
|
|
144
|
+
"args": ["plugins", "install", "clawhub:rol-websocket-channel"],
|
|
145
|
+
"cwd": "/home/user/.openclaw",
|
|
146
|
+
"stdout": "",
|
|
147
|
+
"stderr": ""
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
88
153
|
## 代理管理 (Agents)
|
|
89
154
|
|
|
90
155
|
### agentsGet - 获取代理配置
|
|
@@ -1077,6 +1142,8 @@
|
|
|
1077
1142
|
- `systemStop`
|
|
1078
1143
|
- `systemDoctorFix`
|
|
1079
1144
|
- `systemLogs`
|
|
1145
|
+
- `openclawUpdate`
|
|
1146
|
+
- `pluginSelfUpdate`
|
|
1080
1147
|
- `ping`
|
|
1081
1148
|
- `status`
|
|
1082
1149
|
- `echo`
|
|
@@ -1639,6 +1706,12 @@ openclaw admin-bridge pair <key> --endpoint https://api.deotaland.ai
|
|
|
1639
1706
|
|
|
1640
1707
|
如果文件之前已经上传过,则不会重复上传,而是直接复用已有 `fileUrl`。
|
|
1641
1708
|
|
|
1709
|
+
### Markdown 文件规则
|
|
1710
|
+
|
|
1711
|
+
- 第一次扫描时,已有 `.md` 文件会作为基线记录,不会展示在 artifacts 列表里。
|
|
1712
|
+
- 后续新生成的 `.md` 文件会进入 artifacts 列表。
|
|
1713
|
+
- `MEMORY.md`、`SoUL.md`、`AGENTS.md`、`HEARTBEAT.md`、`IDENTITY.md`、`TooLS.md`、`USER.md` 等记忆/身份文件永远不会展示。
|
|
1714
|
+
|
|
1642
1715
|
|
|
1643
1716
|
|
|
1644
1717
|
|
|
@@ -1658,9 +1731,18 @@ MQTT 请求示例:
|
|
|
1658
1731
|
|
|
1659
1732
|
典型顺序:
|
|
1660
1733
|
|
|
1661
|
-
1. `artifactsList`
|
|
1734
|
+
1. `artifactsList`
|
|
1735
|
+
|
|
1736
|
+
{
|
|
1737
|
+
"type": "artifactsList",
|
|
1738
|
+
"trace_id": "artifact-list-before-download-001",
|
|
1739
|
+
"data": {}
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1662
1742
|
2. 用户选择某个文件
|
|
1743
|
+
|
|
1663
1744
|
3. `artifactsEnsureUploaded`
|
|
1745
|
+
|
|
1664
1746
|
4. 使用返回的 `downloadUrl`
|
|
1665
1747
|
|
|
1666
1748
|
|
|
@@ -1709,3 +1791,9 @@ MQTT 请求示例:
|
|
|
1709
1791
|
3. 直接使用返回的 `data.downloadUrl`
|
|
1710
1792
|
4. 如果返回 `uploaded: false`,表示这次没有重新上传,而是复用了历史上传结果
|
|
1711
1793
|
|
|
1794
|
+
展示建议:
|
|
1795
|
+
|
|
1796
|
+
- `category === "image"` 时,可以作为图片预览/展示。
|
|
1797
|
+
- `category === "document"` 时,可以作为文档展示或下载。
|
|
1798
|
+
- `storageStatus === "local_only"` 时,先调 `artifactsEnsureUploaded` 获取可访问的 `downloadUrl`。
|
|
1799
|
+
- `storageStatus === "uploaded"` 且 `fileUrl` 存在时,可以直接使用 `fileUrl` 展示或下载。
|
package/dist/index.js
CHANGED
|
@@ -512,6 +512,7 @@ function registerAdminBridgeCli(api) {
|
|
|
512
512
|
api.registerCli(({ program }) => {
|
|
513
513
|
const root = program
|
|
514
514
|
.command("admin-bridge")
|
|
515
|
+
.alias("rol-websocket-channel")
|
|
515
516
|
.description("OpenClaw admin bridge utilities")
|
|
516
517
|
.addHelpText("after", () => "\nCommands return JSON to stdout for Python or shell orchestration.\n");
|
|
517
518
|
// 这里可以添加 CLI 命令,但现在先保持简单
|
|
@@ -612,6 +613,11 @@ function registerAdminBridgeCli(api) {
|
|
|
612
613
|
description: "OpenClaw admin bridge commands",
|
|
613
614
|
hasSubcommands: true,
|
|
614
615
|
},
|
|
616
|
+
{
|
|
617
|
+
name: "rol-websocket-channel",
|
|
618
|
+
description: "OpenClaw admin bridge commands",
|
|
619
|
+
hasSubcommands: true,
|
|
620
|
+
},
|
|
615
621
|
],
|
|
616
622
|
});
|
|
617
623
|
}
|
package/dist/message-handler.js
CHANGED
|
@@ -18,7 +18,7 @@ import { uninstallSkill } from './src/admin/methods/skills-extended.js';
|
|
|
18
18
|
import { toggleSkill } from './src/admin/methods/skills-toggle.js';
|
|
19
19
|
import { listMemoryFiles, getMemoryFile, backupMemory, exportMemoryZip, getMemoryPresignedPost, createMemoryBackupRecord, importMemoryZip, } from './src/admin/methods/memory.js';
|
|
20
20
|
import { getMem9Config, installMem9, reconnectMem9 } from './src/admin/methods/mem9.js';
|
|
21
|
-
import {
|
|
21
|
+
import { doctorFix, logs, openclawUpdate, pluginSelfUpdate, restart, stop } from './src/admin/methods/system.js';
|
|
22
22
|
export class MessageHandler {
|
|
23
23
|
/**
|
|
24
24
|
* 示例方法:处理 ping 类型的消息
|
|
@@ -479,6 +479,18 @@ export class MessageHandler {
|
|
|
479
479
|
return await logs(data, context);
|
|
480
480
|
});
|
|
481
481
|
}
|
|
482
|
+
async openclawUpdate(data) {
|
|
483
|
+
return wrapAdminCall(async () => {
|
|
484
|
+
const context = getContext();
|
|
485
|
+
return await openclawUpdate(data, context);
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
async pluginSelfUpdate(data) {
|
|
489
|
+
return wrapAdminCall(async () => {
|
|
490
|
+
const context = getContext();
|
|
491
|
+
return await pluginSelfUpdate(data, context);
|
|
492
|
+
});
|
|
493
|
+
}
|
|
482
494
|
/**
|
|
483
495
|
* 示例方法:处理 status 类型的消息
|
|
484
496
|
*/
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import test from 'node:test';
|
|
4
|
+
import register from '../../index.js';
|
|
5
|
+
const manifest = JSON.parse(readFileSync(new URL('../../openclaw.plugin.json', import.meta.url), 'utf8'));
|
|
6
|
+
test('manifest declares OpenClaw 2026 command ownership for admin bridge CLI', () => {
|
|
7
|
+
assert.deepEqual(manifest.activation?.onCommands, ['admin-bridge', 'rol-websocket-channel']);
|
|
8
|
+
assert.deepEqual(manifest.commandAliases, [
|
|
9
|
+
{ name: 'admin-bridge' },
|
|
10
|
+
{ name: 'rol-websocket-channel' }
|
|
11
|
+
]);
|
|
12
|
+
});
|
|
13
|
+
test('runtime CLI registrar exposes admin-bridge and rol-websocket-channel command roots', () => {
|
|
14
|
+
const descriptors = [];
|
|
15
|
+
const commands = [];
|
|
16
|
+
const aliases = [];
|
|
17
|
+
const commandNode = {
|
|
18
|
+
alias(name) {
|
|
19
|
+
aliases.push(name);
|
|
20
|
+
return commandNode;
|
|
21
|
+
},
|
|
22
|
+
description() {
|
|
23
|
+
return commandNode;
|
|
24
|
+
},
|
|
25
|
+
addHelpText() {
|
|
26
|
+
return commandNode;
|
|
27
|
+
},
|
|
28
|
+
option() {
|
|
29
|
+
return commandNode;
|
|
30
|
+
},
|
|
31
|
+
action() {
|
|
32
|
+
return commandNode;
|
|
33
|
+
},
|
|
34
|
+
command(name) {
|
|
35
|
+
commands.push(name);
|
|
36
|
+
return commandNode;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
const api = {
|
|
40
|
+
runtime: {},
|
|
41
|
+
registerChannel() { },
|
|
42
|
+
registerCli(callback, options) {
|
|
43
|
+
descriptors.push(...options.descriptors);
|
|
44
|
+
callback({ program: commandNode });
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
register(api);
|
|
48
|
+
assert.deepEqual(descriptors, [
|
|
49
|
+
{
|
|
50
|
+
name: 'admin-bridge',
|
|
51
|
+
description: 'OpenClaw admin bridge commands',
|
|
52
|
+
hasSubcommands: true
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: 'rol-websocket-channel',
|
|
56
|
+
description: 'OpenClaw admin bridge commands',
|
|
57
|
+
hasSubcommands: true
|
|
58
|
+
}
|
|
59
|
+
]);
|
|
60
|
+
assert.equal(commands[0], 'admin-bridge');
|
|
61
|
+
assert.equal(aliases[0], 'rol-websocket-channel');
|
|
62
|
+
});
|
|
@@ -7,8 +7,19 @@ import { JsonRpcException, JSON_RPC_ERRORS } from '../jsonrpc.js';
|
|
|
7
7
|
const INLINE_CONTENT_LIMIT_BYTES = 10 * 1024 * 1024;
|
|
8
8
|
const MIN_ARTIFACT_SIZE_BYTES = 1;
|
|
9
9
|
const ARTIFACT_MANIFEST_FILE = 'artifacts.json';
|
|
10
|
+
const MARKDOWN_SCAN_STATE_FILE = 'md-scan-state.json';
|
|
11
|
+
const PLUGIN_WORKSPACE_STATE_DIR = path.join('.openclaw', 'rol-websocket-channel');
|
|
10
12
|
const IGNORE_EXTENSIONS = new Set(['.tmp', '.part', '.crdownload']);
|
|
11
13
|
const IGNORE_FILE_NAMES = new Set(['.ds_store', 'thumbs.db', ARTIFACT_MANIFEST_FILE]);
|
|
14
|
+
const IGNORE_MARKDOWN_FILE_NAMES = new Set([
|
|
15
|
+
'agents.md',
|
|
16
|
+
'heartbeat.md',
|
|
17
|
+
'identity.md',
|
|
18
|
+
'memory.md',
|
|
19
|
+
'soul.md',
|
|
20
|
+
'tools.md',
|
|
21
|
+
'user.md'
|
|
22
|
+
]);
|
|
12
23
|
const IGNORE_DIRECTORY_NAMES = new Set([
|
|
13
24
|
'.git',
|
|
14
25
|
'.cache',
|
|
@@ -39,6 +50,7 @@ const CATEGORY_BY_EXTENSION = {
|
|
|
39
50
|
'.pdf': 'document',
|
|
40
51
|
'.doc': 'document',
|
|
41
52
|
'.docx': 'document',
|
|
53
|
+
'.md': 'document',
|
|
42
54
|
'.zip': 'archive',
|
|
43
55
|
'.7z': 'archive',
|
|
44
56
|
'.tar': 'archive',
|
|
@@ -61,6 +73,7 @@ const MIME_BY_EXTENSION = {
|
|
|
61
73
|
'.pdf': 'application/pdf',
|
|
62
74
|
'.doc': 'application/msword',
|
|
63
75
|
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
76
|
+
'.md': 'text/markdown',
|
|
64
77
|
'.zip': 'application/zip',
|
|
65
78
|
'.7z': 'application/x-7z-compressed',
|
|
66
79
|
'.tar': 'application/x-tar',
|
|
@@ -238,6 +251,9 @@ async function refreshArtifactManifest(context) {
|
|
|
238
251
|
const { workspaceRoot, manifestPath } = workspacePaths;
|
|
239
252
|
const existing = await readExistingManifest(manifestPath);
|
|
240
253
|
const existingByPath = new Map(existing.map((item) => [item.relativePath, item]));
|
|
254
|
+
const loadedMarkdownScanState = await readMarkdownScanState(workspacePaths.markdownScanStatePath);
|
|
255
|
+
const hadMarkdownScanState = loadedMarkdownScanState !== null;
|
|
256
|
+
const markdownScanState = loadedMarkdownScanState ?? createMarkdownScanState();
|
|
241
257
|
const files = await collectArtifactFiles(workspaceRoot);
|
|
242
258
|
const items = [];
|
|
243
259
|
for (const fullPath of files) {
|
|
@@ -248,7 +264,13 @@ async function refreshArtifactManifest(context) {
|
|
|
248
264
|
const relativePath = path.relative(workspaceRoot, fullPath).replace(/\\/g, '/');
|
|
249
265
|
const existingItem = existingByPath.get(relativePath);
|
|
250
266
|
const fileName = path.basename(fullPath);
|
|
251
|
-
|
|
267
|
+
const isMarkdown = isMarkdownArtifactFile(fileName);
|
|
268
|
+
if (isMarkdown) {
|
|
269
|
+
if (!shouldIncludeMarkdownArtifactFile(relativePath, fileName, stat, markdownScanState, hadMarkdownScanState)) {
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
else if (!shouldIncludeArtifactFile(fileName)) {
|
|
252
274
|
continue;
|
|
253
275
|
}
|
|
254
276
|
const ext = normalizeExtension(fileName);
|
|
@@ -275,6 +297,8 @@ async function refreshArtifactManifest(context) {
|
|
|
275
297
|
});
|
|
276
298
|
}
|
|
277
299
|
items.sort((a, b) => a.fileName.localeCompare(b.fileName));
|
|
300
|
+
markdownScanState.lastScanAt = new Date().toISOString();
|
|
301
|
+
await writeMarkdownScanState(workspacePaths.markdownScanStatePath, markdownScanState);
|
|
278
302
|
await writeArtifactManifest(manifestPath, items);
|
|
279
303
|
return { manifestPath, items, workspacePaths };
|
|
280
304
|
}
|
|
@@ -306,6 +330,32 @@ async function readExistingManifest(manifestPath) {
|
|
|
306
330
|
async function writeArtifactManifest(manifestPath, items) {
|
|
307
331
|
await fs.writeFile(manifestPath, JSON.stringify(items.map((item) => artifactToJsonValue(item)), null, 2), 'utf8');
|
|
308
332
|
}
|
|
333
|
+
async function readMarkdownScanState(statePath) {
|
|
334
|
+
if (!(await pathExists(statePath))) {
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
const parsed = await readJsonFile(statePath);
|
|
338
|
+
if (!isObject(parsed)) {
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
const files = {};
|
|
342
|
+
if (isObject(parsed.files)) {
|
|
343
|
+
for (const [relativePath, value] of Object.entries(parsed.files)) {
|
|
344
|
+
if (isMarkdownScanStateFile(value)) {
|
|
345
|
+
files[relativePath] = value;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return {
|
|
350
|
+
initializedAt: typeof parsed.initializedAt === 'string' ? parsed.initializedAt : new Date().toISOString(),
|
|
351
|
+
lastScanAt: typeof parsed.lastScanAt === 'string' ? parsed.lastScanAt : new Date().toISOString(),
|
|
352
|
+
files
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
async function writeMarkdownScanState(statePath, state) {
|
|
356
|
+
await ensureDir(path.dirname(statePath));
|
|
357
|
+
await fs.writeFile(statePath, JSON.stringify(state, null, 2), 'utf8');
|
|
358
|
+
}
|
|
309
359
|
async function persistUploadedArtifact(manifestPath, items, artifactId, updates) {
|
|
310
360
|
const now = new Date().toISOString();
|
|
311
361
|
let updatedArtifact = null;
|
|
@@ -393,10 +443,20 @@ function resolveWorkspaceRoot(openclawRoot) {
|
|
|
393
443
|
async function ensureWorkspacePaths(openclawRoot) {
|
|
394
444
|
const workspaceRoot = resolveWorkspaceRoot(openclawRoot);
|
|
395
445
|
const manifestPath = ensureInside(workspaceRoot, path.join(workspaceRoot, ARTIFACT_MANIFEST_FILE));
|
|
446
|
+
const markdownScanStatePath = ensureInside(workspaceRoot, path.join(workspaceRoot, PLUGIN_WORKSPACE_STATE_DIR, MARKDOWN_SCAN_STATE_FILE));
|
|
396
447
|
await ensureDir(workspaceRoot);
|
|
397
448
|
return {
|
|
398
449
|
workspaceRoot,
|
|
399
|
-
manifestPath
|
|
450
|
+
manifestPath,
|
|
451
|
+
markdownScanStatePath
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
function createMarkdownScanState() {
|
|
455
|
+
const now = new Date().toISOString();
|
|
456
|
+
return {
|
|
457
|
+
initializedAt: now,
|
|
458
|
+
lastScanAt: now,
|
|
459
|
+
files: {}
|
|
400
460
|
};
|
|
401
461
|
}
|
|
402
462
|
function buildArtifactId(relativePath) {
|
|
@@ -406,6 +466,9 @@ function normalizeExtension(fileName) {
|
|
|
406
466
|
const ext = path.extname(fileName).toLowerCase();
|
|
407
467
|
return ext || null;
|
|
408
468
|
}
|
|
469
|
+
function isMarkdownArtifactFile(fileName) {
|
|
470
|
+
return path.extname(fileName).toLowerCase() === '.md';
|
|
471
|
+
}
|
|
409
472
|
function normalizeRelativePath(value) {
|
|
410
473
|
if (!value) {
|
|
411
474
|
return null;
|
|
@@ -437,6 +500,37 @@ function shouldIncludeArtifactFile(fileName) {
|
|
|
437
500
|
}
|
|
438
501
|
return classifyArtifactCategory(fileName) !== 'other';
|
|
439
502
|
}
|
|
503
|
+
function shouldIncludeMarkdownArtifactFile(relativePath, fileName, stat, state, hadState) {
|
|
504
|
+
if (shouldIgnoreArtifactFile(fileName)) {
|
|
505
|
+
return false;
|
|
506
|
+
}
|
|
507
|
+
if (IGNORE_MARKDOWN_FILE_NAMES.has(fileName.trim().toLowerCase())) {
|
|
508
|
+
return false;
|
|
509
|
+
}
|
|
510
|
+
const existing = state.files[relativePath];
|
|
511
|
+
if (existing) {
|
|
512
|
+
existing.sizeBytes = stat.size;
|
|
513
|
+
existing.mtimeMs = stat.mtimeMs;
|
|
514
|
+
return !existing.baseline;
|
|
515
|
+
}
|
|
516
|
+
state.files[relativePath] = {
|
|
517
|
+
sizeBytes: stat.size,
|
|
518
|
+
mtimeMs: stat.mtimeMs,
|
|
519
|
+
firstSeenAt: new Date().toISOString(),
|
|
520
|
+
baseline: !hadState
|
|
521
|
+
};
|
|
522
|
+
return hadState;
|
|
523
|
+
}
|
|
524
|
+
function isMarkdownScanStateFile(value) {
|
|
525
|
+
if (!value || Array.isArray(value) || typeof value !== 'object') {
|
|
526
|
+
return false;
|
|
527
|
+
}
|
|
528
|
+
const objectValue = value;
|
|
529
|
+
return (typeof objectValue.sizeBytes === 'number' &&
|
|
530
|
+
typeof objectValue.mtimeMs === 'number' &&
|
|
531
|
+
typeof objectValue.firstSeenAt === 'string' &&
|
|
532
|
+
typeof objectValue.baseline === 'boolean');
|
|
533
|
+
}
|
|
440
534
|
function isArtifactRecord(value) {
|
|
441
535
|
if (!value || Array.isArray(value) || typeof value !== 'object') {
|
|
442
536
|
return false;
|
|
@@ -45,6 +45,25 @@ describe('artifacts workspace scope', () => {
|
|
|
45
45
|
assert.equal(result.workspaceRoot, workspaceRoot);
|
|
46
46
|
assert.equal(result.manifestPath, path.join(workspaceRoot, 'artifacts.json'));
|
|
47
47
|
});
|
|
48
|
+
test('indexes only markdown files added after the initial baseline', async () => {
|
|
49
|
+
const context = await createMethodContext();
|
|
50
|
+
const workspaceRoot = path.join(context.openclawRoot, 'workspace');
|
|
51
|
+
await fs.mkdir(path.join(workspaceRoot, 'docs'), { recursive: true });
|
|
52
|
+
await fs.writeFile(path.join(workspaceRoot, 'docs', 'existing.md'), '# existing\n', 'utf8');
|
|
53
|
+
await fs.writeFile(path.join(workspaceRoot, 'MEMORY.md'), '# memory\n', 'utf8');
|
|
54
|
+
await fs.writeFile(path.join(workspaceRoot, 'SoUL.md'), '# soul\n', 'utf8');
|
|
55
|
+
const baseline = await refreshArtifacts({}, context);
|
|
56
|
+
assert.equal(baseline.count, 0);
|
|
57
|
+
assert.deepEqual(baseline.items, []);
|
|
58
|
+
await fs.writeFile(path.join(workspaceRoot, 'docs', 'existing.md'), '# changed\n', 'utf8');
|
|
59
|
+
await fs.writeFile(path.join(workspaceRoot, 'MEMORY.md'), '# changed memory\n', 'utf8');
|
|
60
|
+
await fs.writeFile(path.join(workspaceRoot, 'docs', 'generated.md'), '# generated\n', 'utf8');
|
|
61
|
+
const refreshed = await refreshArtifacts({}, context);
|
|
62
|
+
assert.equal(refreshed.count, 1);
|
|
63
|
+
assert.deepEqual(refreshed.items.map((item) => item.relativePath), ['docs/generated.md']);
|
|
64
|
+
assert.equal(refreshed.items[0]?.category, 'document');
|
|
65
|
+
assert.equal(refreshed.items[0]?.mimeType, 'text/markdown');
|
|
66
|
+
});
|
|
48
67
|
test('list reads workspace manifest without taskId', async () => {
|
|
49
68
|
const context = await createMethodContext();
|
|
50
69
|
const workspaceRoot = path.join(context.openclawRoot, 'workspace');
|
|
@@ -11,7 +11,7 @@ import { listSessions } from './sessions.js';
|
|
|
11
11
|
import { getSession, prepareMessage, attachSkill } from './sessions-extended.js';
|
|
12
12
|
import { installSkillFromClawHub, installSkillFromNpm, listInstalledSkills, searchClawHubSkills, updateSkillFromClawHub } from './skills.js';
|
|
13
13
|
import { getInstalledSkill, uninstallSkill } from './skills-extended.js';
|
|
14
|
-
import {
|
|
14
|
+
import { doctorFix, logs, openclawUpdate, ping, pluginSelfUpdate, restart, stop } from './system.js';
|
|
15
15
|
import { getUsageBreakdown, getUsagePageSummary, getUsageSummary, getUsageTimeseries } from './usage.js';
|
|
16
16
|
const methods = new Map([
|
|
17
17
|
// System
|
|
@@ -20,6 +20,8 @@ const methods = new Map([
|
|
|
20
20
|
['system.stop', stop],
|
|
21
21
|
['system.doctorFix', doctorFix],
|
|
22
22
|
['system.logs', logs],
|
|
23
|
+
['system.openclawUpdate', openclawUpdate],
|
|
24
|
+
['system.pluginSelfUpdate', pluginSelfUpdate],
|
|
23
25
|
// Agents
|
|
24
26
|
['agents.get', getAgents],
|
|
25
27
|
['agents.list', listAgents],
|
|
@@ -9,14 +9,28 @@ const MEM9_PLUGIN_ID = 'mem9';
|
|
|
9
9
|
const MEM9_API_URL = 'https://api.mem9.ai';
|
|
10
10
|
const MEM9_CREATE_URL = `${MEM9_API_URL}/v1alpha1/mem9s`;
|
|
11
11
|
const GATEWAY_SERVICE = 'openclaw-gateway.service';
|
|
12
|
+
const MEM9_PACKAGE_ROOTS = [
|
|
13
|
+
path.join('npm', 'node_modules', '@mem9', 'mem9'),
|
|
14
|
+
path.join('npm', 'node_modules', 'mem9')
|
|
15
|
+
];
|
|
16
|
+
const RUNTIME_ENTRYPOINTS = [
|
|
17
|
+
path.join('dist', 'index.js'),
|
|
18
|
+
path.join('dist', 'index.mjs'),
|
|
19
|
+
path.join('dist', 'index.cjs'),
|
|
20
|
+
'index.js',
|
|
21
|
+
'index.mjs',
|
|
22
|
+
'index.cjs'
|
|
23
|
+
];
|
|
12
24
|
export async function installMem9(context) {
|
|
13
25
|
const config = await ensureOpenClawConfigExists(context.openclawRoot);
|
|
14
26
|
await ensureOpenClawCli();
|
|
15
27
|
await ensureNodeRuntime();
|
|
16
28
|
const currentState = readMem9State(config);
|
|
17
|
-
const
|
|
29
|
+
const currentEntrypoint = await findMem9RuntimeEntrypoint(context.openclawRoot);
|
|
30
|
+
const installResult = currentState.installed && currentEntrypoint
|
|
18
31
|
? { attempted: false, installed: true }
|
|
19
32
|
: await installMem9Plugin(context.projectRoot);
|
|
33
|
+
const runtimeEntrypoint = await ensureMem9RuntimeEntrypoint(context.openclawRoot);
|
|
20
34
|
if (currentState.configured && currentState.apiKey) {
|
|
21
35
|
const updated = await ensureMem9SlotConfig(context.openclawRoot, currentState.apiKey);
|
|
22
36
|
const restart = await restartGateway(context.projectRoot);
|
|
@@ -28,6 +42,7 @@ export async function installMem9(context) {
|
|
|
28
42
|
createdNewKey: false,
|
|
29
43
|
reusedExistingKey: true,
|
|
30
44
|
plugin: MEM9_PLUGIN_ID,
|
|
45
|
+
runtimeEntrypoint,
|
|
31
46
|
apiUrl: MEM9_API_URL,
|
|
32
47
|
apiKey: currentState.apiKey,
|
|
33
48
|
updated,
|
|
@@ -45,6 +60,7 @@ export async function installMem9(context) {
|
|
|
45
60
|
createdNewKey: true,
|
|
46
61
|
reusedExistingKey: false,
|
|
47
62
|
plugin: MEM9_PLUGIN_ID,
|
|
63
|
+
runtimeEntrypoint,
|
|
48
64
|
apiUrl: MEM9_API_URL,
|
|
49
65
|
apiKey,
|
|
50
66
|
updated,
|
|
@@ -58,6 +74,7 @@ export async function reconnectMem9(key, context) {
|
|
|
58
74
|
}
|
|
59
75
|
const config = await ensureOpenClawConfigExists(context.openclawRoot);
|
|
60
76
|
const previousState = readMem9State(config);
|
|
77
|
+
const runtimeEntrypoint = await ensureMem9RuntimeEntrypoint(context.openclawRoot);
|
|
61
78
|
const updated = await writeMem9Config(context.openclawRoot, apiKey);
|
|
62
79
|
const restart = await restartGateway(context.projectRoot);
|
|
63
80
|
return {
|
|
@@ -65,6 +82,7 @@ export async function reconnectMem9(key, context) {
|
|
|
65
82
|
reconnected: true,
|
|
66
83
|
replacedExistingKey: Boolean(previousState.apiKey && previousState.apiKey !== apiKey),
|
|
67
84
|
plugin: MEM9_PLUGIN_ID,
|
|
85
|
+
runtimeEntrypoint,
|
|
68
86
|
apiUrl: MEM9_API_URL,
|
|
69
87
|
apiKey,
|
|
70
88
|
updated: ['plugins.entries.mem9.config.apiKey', ...updated.filter((item) => item !== 'plugins.entries.mem9' && item !== 'plugins.slots.memory')],
|
|
@@ -130,6 +148,27 @@ async function installMem9Plugin(cwd) {
|
|
|
130
148
|
installed: true
|
|
131
149
|
};
|
|
132
150
|
}
|
|
151
|
+
export async function findMem9RuntimeEntrypoint(openclawRoot) {
|
|
152
|
+
for (const packageRoot of MEM9_PACKAGE_ROOTS.map((item) => path.join(openclawRoot, item))) {
|
|
153
|
+
for (const entrypoint of RUNTIME_ENTRYPOINTS.map((item) => path.join(packageRoot, item))) {
|
|
154
|
+
if (await pathExists(entrypoint)) {
|
|
155
|
+
return entrypoint;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
async function ensureMem9RuntimeEntrypoint(openclawRoot) {
|
|
162
|
+
const entrypoint = await findMem9RuntimeEntrypoint(openclawRoot);
|
|
163
|
+
if (entrypoint) {
|
|
164
|
+
return entrypoint;
|
|
165
|
+
}
|
|
166
|
+
throw new JsonRpcException(JSON_RPC_ERRORS.internalError, 'mem9 plugin is installed but missing compiled runtime output required by OpenClaw 2026.5.6', {
|
|
167
|
+
code: 'MEM9_RUNTIME_OUTPUT_MISSING',
|
|
168
|
+
expected: RUNTIME_ENTRYPOINTS.map((item) => `./${item.replace(/\\/g, '/')}`),
|
|
169
|
+
packageRoots: MEM9_PACKAGE_ROOTS.map((item) => path.join(openclawRoot, item))
|
|
170
|
+
});
|
|
171
|
+
}
|
|
133
172
|
async function createMem9Key() {
|
|
134
173
|
const response = await fetch(MEM9_CREATE_URL, {
|
|
135
174
|
method: 'POST',
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { findMem9RuntimeEntrypoint } from './mem9.js';
|
|
7
|
+
describe('mem9 runtime compatibility', () => {
|
|
8
|
+
test('accepts compiled runtime output under the installed package', async () => {
|
|
9
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'mem9-runtime-'));
|
|
10
|
+
try {
|
|
11
|
+
const packageRoot = path.join(root, '.openclaw', 'npm', 'node_modules', '@mem9', 'mem9');
|
|
12
|
+
await fs.mkdir(path.join(packageRoot, 'dist'), { recursive: true });
|
|
13
|
+
await fs.writeFile(path.join(packageRoot, 'dist', 'index.js'), 'export default {};\n', 'utf8');
|
|
14
|
+
const entrypoint = await findMem9RuntimeEntrypoint(path.join(root, '.openclaw'));
|
|
15
|
+
assert.equal(entrypoint, path.join(packageRoot, 'dist', 'index.js'));
|
|
16
|
+
}
|
|
17
|
+
finally {
|
|
18
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
test('rejects TypeScript-only mem9 packages before writing memory slot', async () => {
|
|
22
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'mem9-runtime-'));
|
|
23
|
+
try {
|
|
24
|
+
const packageRoot = path.join(root, '.openclaw', 'npm', 'node_modules', '@mem9', 'mem9');
|
|
25
|
+
await fs.mkdir(packageRoot, { recursive: true });
|
|
26
|
+
await fs.writeFile(path.join(packageRoot, 'index.ts'), 'export default {};\n', 'utf8');
|
|
27
|
+
const entrypoint = await findMem9RuntimeEntrypoint(path.join(root, '.openclaw'));
|
|
28
|
+
assert.equal(entrypoint, null);
|
|
29
|
+
}
|
|
30
|
+
finally {
|
|
31
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
import { exec } from 'node:child_process';
|
|
1
|
+
import { exec, execFile } from 'node:child_process';
|
|
2
2
|
import fs from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { promisify } from 'node:util';
|
|
5
|
+
import { JsonRpcException, JSON_RPC_ERRORS } from '../jsonrpc.js';
|
|
5
6
|
const execAsync = promisify(exec);
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
8
|
+
const UPDATE_COMMAND_TIMEOUT_MS = 10 * 60 * 1000;
|
|
9
|
+
const UPDATE_COMMAND_MAX_BUFFER = 10 * 1024 * 1024;
|
|
6
10
|
export const ping = async () => {
|
|
7
11
|
return {
|
|
8
12
|
ok: true,
|
|
@@ -67,6 +71,25 @@ export const doctorFix = async (_params, context) => {
|
|
|
67
71
|
};
|
|
68
72
|
}
|
|
69
73
|
};
|
|
74
|
+
export const openclawUpdate = async (_params, context) => {
|
|
75
|
+
const result = await runOpenClawCommand(['update'], context, 'openclawUpdate');
|
|
76
|
+
return {
|
|
77
|
+
ok: true,
|
|
78
|
+
action: 'openclawUpdate',
|
|
79
|
+
restartRecommended: true,
|
|
80
|
+
...result
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
export const pluginSelfUpdate = async (_params, context) => {
|
|
84
|
+
const result = await runOpenClawCommand(['plugins', 'install', 'clawhub:rol-websocket-channel'], context, 'pluginSelfUpdate');
|
|
85
|
+
return {
|
|
86
|
+
ok: true,
|
|
87
|
+
action: 'pluginSelfUpdate',
|
|
88
|
+
plugin: 'clawhub:rol-websocket-channel',
|
|
89
|
+
restartRecommended: true,
|
|
90
|
+
...result
|
|
91
|
+
};
|
|
92
|
+
};
|
|
70
93
|
export const logs = async (params, context) => {
|
|
71
94
|
try {
|
|
72
95
|
// 根据实际情况可能需要调整,这里默认尝试 project root 或 user home 下的 .openclaw/logs
|
|
@@ -178,3 +201,71 @@ export const logs = async (params, context) => {
|
|
|
178
201
|
};
|
|
179
202
|
}
|
|
180
203
|
};
|
|
204
|
+
async function runOpenClawCommand(args, context, action) {
|
|
205
|
+
const command = process.env.OPENCLAW_BIN || 'openclaw';
|
|
206
|
+
const options = buildOpenClawExecOptions(context.openclawRoot, context.openclawRoot);
|
|
207
|
+
const openclawBin = process.env.OPENCLAW_BIN || '';
|
|
208
|
+
const openclawHome = options.env?.OPENCLAW_HOME || '';
|
|
209
|
+
const home = options.env?.HOME || '';
|
|
210
|
+
console.log(`[system] exec start: action=${action}, command=${command}, args=${JSON.stringify(args)}, cwd=${options.cwd}, OPENCLAW_BIN=${openclawBin}, OPENCLAW_HOME=${openclawHome}, HOME=${home}`);
|
|
211
|
+
try {
|
|
212
|
+
const { stdout, stderr } = await execFileAsync(command, args, {
|
|
213
|
+
...options,
|
|
214
|
+
timeout: UPDATE_COMMAND_TIMEOUT_MS,
|
|
215
|
+
maxBuffer: UPDATE_COMMAND_MAX_BUFFER
|
|
216
|
+
});
|
|
217
|
+
console.log(`[system] exec success: action=${action}, command=${command}, args=${JSON.stringify(args)}, stdoutLength=${stdout.length}, stderrLength=${stderr.length}`);
|
|
218
|
+
return {
|
|
219
|
+
command,
|
|
220
|
+
args,
|
|
221
|
+
cwd: options.cwd,
|
|
222
|
+
stdout,
|
|
223
|
+
stderr
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
catch (err) {
|
|
227
|
+
const stdout = typeof err?.stdout === 'string' ? err.stdout : '';
|
|
228
|
+
const stderr = typeof err?.stderr === 'string' ? err.stderr : '';
|
|
229
|
+
console.error(`[system] exec failed: action=${action}, command=${command}, args=${JSON.stringify(args)}, cwd=${options.cwd}, OPENCLAW_BIN=${openclawBin}, OPENCLAW_HOME=${openclawHome}, HOME=${home}, stdout=${JSON.stringify(stdout)}, stderr=${JSON.stringify(stderr)}`);
|
|
230
|
+
throw new JsonRpcException(JSON_RPC_ERRORS.internalError, `OpenClaw system command failed: ${err instanceof Error ? err.message : String(err)}`, {
|
|
231
|
+
action,
|
|
232
|
+
command,
|
|
233
|
+
args,
|
|
234
|
+
cwd: options.cwd,
|
|
235
|
+
stdout,
|
|
236
|
+
stderr
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function buildOpenClawExecOptions(cwd, openclawRoot) {
|
|
241
|
+
const env = { ...process.env };
|
|
242
|
+
const openclawHome = resolveOpenClawHomeForCli(openclawRoot);
|
|
243
|
+
if (openclawHome) {
|
|
244
|
+
env.OPENCLAW_HOME = openclawHome;
|
|
245
|
+
}
|
|
246
|
+
if (openclawRoot && path.basename(path.resolve(openclawRoot)) === '.openclaw') {
|
|
247
|
+
const home = path.dirname(path.resolve(openclawRoot));
|
|
248
|
+
env.HOME = home;
|
|
249
|
+
if (process.platform === 'win32') {
|
|
250
|
+
env.USERPROFILE = home;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (process.platform === 'win32') {
|
|
254
|
+
return {
|
|
255
|
+
cwd,
|
|
256
|
+
shell: true,
|
|
257
|
+
env
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
return { cwd, env };
|
|
261
|
+
}
|
|
262
|
+
function resolveOpenClawHomeForCli(openclawRoot) {
|
|
263
|
+
if (!openclawRoot) {
|
|
264
|
+
return process.env.OPENCLAW_HOME;
|
|
265
|
+
}
|
|
266
|
+
const resolvedRoot = path.resolve(openclawRoot);
|
|
267
|
+
if (path.basename(resolvedRoot) === '.openclaw') {
|
|
268
|
+
return path.dirname(resolvedRoot);
|
|
269
|
+
}
|
|
270
|
+
return process.env.OPENCLAW_HOME;
|
|
271
|
+
}
|