rol-websocket-channel 1.4.2 → 1.4.8
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 +617 -617
- package/dist/message-handler.js +515 -503
- package/dist/src/admin/cli.js +43 -43
- package/dist/src/admin/jsonrpc.js +60 -60
- package/dist/src/admin/lib/fs.js +30 -30
- package/dist/src/admin/lib/paths.js +80 -80
- package/dist/src/admin/methods/admin.js +60 -60
- package/dist/src/admin/methods/agents-extended.js +251 -251
- package/dist/src/admin/methods/artifacts.js +736 -642
- package/dist/src/admin/methods/artifacts.test.js +210 -191
- package/dist/src/admin/methods/cron.js +250 -250
- package/dist/src/admin/methods/index.js +104 -102
- package/dist/src/admin/methods/mem9.js +309 -270
- package/dist/src/admin/methods/mem9.test.js +34 -0
- package/dist/src/admin/methods/memory.js +363 -363
- package/dist/src/admin/methods/models-extended.js +190 -190
- package/dist/src/admin/methods/models.js +195 -195
- package/dist/src/admin/methods/pairing.js +268 -268
- package/dist/src/admin/methods/sessions-extended.js +215 -215
- package/dist/src/admin/methods/sessions.js +75 -75
- package/dist/src/admin/methods/skills-extended.js +157 -157
- package/dist/src/admin/methods/skills-toggle.js +183 -183
- package/dist/src/admin/methods/skills.js +528 -528
- package/dist/src/admin/methods/system.js +271 -180
- package/dist/src/admin/methods/usage.js +1170 -1170
- package/dist/src/admin/types.js +1 -1
- package/dist/src/mqtt/connection-manager.js +209 -209
- package/dist/src/mqtt/index.js +5 -5
- package/dist/src/mqtt/mqtt-client.js +110 -110
- package/dist/src/mqtt/mqtt.test.js +418 -418
- package/dist/src/mqtt/types.js +2 -2
- package/dist/src/shared/context.js +24 -24
- package/dist/src/shared/wrapper.js +23 -23
- package/message-handler.ts +15 -1
- package/openclaw.plugin.json +73 -0
- package/package.json +1 -1
- 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
package/dist/src/mqtt/types.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
// MQTT 连接管理类型定义
|
|
2
|
-
export {};
|
|
1
|
+
// MQTT 连接管理类型定义
|
|
2
|
+
export {};
|
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 共享 Context 管理
|
|
3
|
-
* 用于在 MQTT handler 和 admin 方法之间共享上下文
|
|
4
|
-
*/
|
|
5
|
-
import { getOpenClawRoot, getProjectRoot } from '../admin/lib/paths.js';
|
|
6
|
-
let cachedContext = null;
|
|
7
|
-
/**
|
|
8
|
-
* 初始化全局 context
|
|
9
|
-
*/
|
|
10
|
-
export function initializeContext() {
|
|
11
|
-
cachedContext = {
|
|
12
|
-
projectRoot: getProjectRoot(),
|
|
13
|
-
openclawRoot: getOpenClawRoot(),
|
|
14
|
-
};
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* 获取当前 context
|
|
18
|
-
*/
|
|
19
|
-
export function getContext() {
|
|
20
|
-
if (!cachedContext) {
|
|
21
|
-
initializeContext();
|
|
22
|
-
}
|
|
23
|
-
return cachedContext;
|
|
24
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* 共享 Context 管理
|
|
3
|
+
* 用于在 MQTT handler 和 admin 方法之间共享上下文
|
|
4
|
+
*/
|
|
5
|
+
import { getOpenClawRoot, getProjectRoot } from '../admin/lib/paths.js';
|
|
6
|
+
let cachedContext = null;
|
|
7
|
+
/**
|
|
8
|
+
* 初始化全局 context
|
|
9
|
+
*/
|
|
10
|
+
export function initializeContext() {
|
|
11
|
+
cachedContext = {
|
|
12
|
+
projectRoot: getProjectRoot(),
|
|
13
|
+
openclawRoot: getOpenClawRoot(),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* 获取当前 context
|
|
18
|
+
*/
|
|
19
|
+
export function getContext() {
|
|
20
|
+
if (!cachedContext) {
|
|
21
|
+
initializeContext();
|
|
22
|
+
}
|
|
23
|
+
return cachedContext;
|
|
24
|
+
}
|
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 统一的方法调用包装器
|
|
3
|
-
* 提供统一的错误处理和返回格式
|
|
4
|
-
*/
|
|
5
|
-
/**
|
|
6
|
-
* 包装 admin 方法调用,统一返回格式
|
|
7
|
-
*/
|
|
8
|
-
export async function wrapAdminCall(fn) {
|
|
9
|
-
try {
|
|
10
|
-
const result = await fn();
|
|
11
|
-
return { ok: true, result };
|
|
12
|
-
}
|
|
13
|
-
catch (error) {
|
|
14
|
-
return {
|
|
15
|
-
ok: false,
|
|
16
|
-
error: {
|
|
17
|
-
message: error instanceof Error ? error.message : String(error),
|
|
18
|
-
code: error.code,
|
|
19
|
-
data: error.data,
|
|
20
|
-
},
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* 统一的方法调用包装器
|
|
3
|
+
* 提供统一的错误处理和返回格式
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* 包装 admin 方法调用,统一返回格式
|
|
7
|
+
*/
|
|
8
|
+
export async function wrapAdminCall(fn) {
|
|
9
|
+
try {
|
|
10
|
+
const result = await fn();
|
|
11
|
+
return { ok: true, result };
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
return {
|
|
15
|
+
ok: false,
|
|
16
|
+
error: {
|
|
17
|
+
message: error instanceof Error ? error.message : String(error),
|
|
18
|
+
code: error.code,
|
|
19
|
+
data: error.data,
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
}
|
package/message-handler.ts
CHANGED
|
@@ -60,7 +60,7 @@ import {
|
|
|
60
60
|
importMemoryZip,
|
|
61
61
|
} from './src/admin/methods/memory.js';
|
|
62
62
|
import { getMem9Config, installMem9, reconnectMem9 } from './src/admin/methods/mem9.js';
|
|
63
|
-
import {
|
|
63
|
+
import { doctorFix, logs, openclawUpdate, pluginSelfUpdate, restart, stop } from './src/admin/methods/system.js';
|
|
64
64
|
|
|
65
65
|
export class MessageHandler {
|
|
66
66
|
/**
|
|
@@ -583,6 +583,20 @@ export class MessageHandler {
|
|
|
583
583
|
});
|
|
584
584
|
}
|
|
585
585
|
|
|
586
|
+
async openclawUpdate(data: any): Promise<any> {
|
|
587
|
+
return wrapAdminCall(async () => {
|
|
588
|
+
const context = getContext();
|
|
589
|
+
return await openclawUpdate(data, context);
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
async pluginSelfUpdate(data: any): Promise<any> {
|
|
594
|
+
return wrapAdminCall(async () => {
|
|
595
|
+
const context = getContext();
|
|
596
|
+
return await pluginSelfUpdate(data, context);
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
|
|
586
600
|
/**
|
|
587
601
|
* 示例方法:处理 status 类型的消息
|
|
588
602
|
*/
|
package/openclaw.plugin.json
CHANGED
|
@@ -4,6 +4,79 @@
|
|
|
4
4
|
"version": "1.0.0",
|
|
5
5
|
"description": "Unified plugin providing MQTT Channel and Admin Bridge capabilities for OpenClaw management",
|
|
6
6
|
"channels": ["rol-websocket-channel"],
|
|
7
|
+
"channelConfigs": {
|
|
8
|
+
"rol-websocket-channel": {
|
|
9
|
+
"schema": {
|
|
10
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
11
|
+
"type": "object",
|
|
12
|
+
"properties": {
|
|
13
|
+
"enabled": {
|
|
14
|
+
"type": "boolean"
|
|
15
|
+
},
|
|
16
|
+
"pairingEndpoint": {
|
|
17
|
+
"type": "string"
|
|
18
|
+
},
|
|
19
|
+
"mqttUrl": {
|
|
20
|
+
"type": "string"
|
|
21
|
+
},
|
|
22
|
+
"mqttTopic": {
|
|
23
|
+
"type": "string"
|
|
24
|
+
},
|
|
25
|
+
"groupPolicy": {
|
|
26
|
+
"type": "string",
|
|
27
|
+
"enum": ["pairing", "allowlist", "open", "disabled"]
|
|
28
|
+
},
|
|
29
|
+
"config": {
|
|
30
|
+
"type": "object",
|
|
31
|
+
"properties": {
|
|
32
|
+
"enabled": {
|
|
33
|
+
"type": "boolean"
|
|
34
|
+
},
|
|
35
|
+
"pairingEndpoint": {
|
|
36
|
+
"type": "string"
|
|
37
|
+
},
|
|
38
|
+
"mqttUrl": {
|
|
39
|
+
"type": "string"
|
|
40
|
+
},
|
|
41
|
+
"mqttTopic": {
|
|
42
|
+
"type": "string"
|
|
43
|
+
},
|
|
44
|
+
"groupPolicy": {
|
|
45
|
+
"type": "string",
|
|
46
|
+
"enum": ["pairing", "allowlist", "open", "disabled"]
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"additionalProperties": true
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"additionalProperties": true
|
|
53
|
+
},
|
|
54
|
+
"uiHints": {
|
|
55
|
+
"enabled": {
|
|
56
|
+
"label": "Enabled",
|
|
57
|
+
"description": "Enable this configuration"
|
|
58
|
+
},
|
|
59
|
+
"pairingEndpoint": {
|
|
60
|
+
"label": "Pairing Endpoint",
|
|
61
|
+
"description": "Optional pairing API endpoint or base URL for staging/local environments"
|
|
62
|
+
},
|
|
63
|
+
"mqttUrl": {
|
|
64
|
+
"label": "MQTT Broker URL",
|
|
65
|
+
"placeholder": "ws://192.168.1.152:8083/mqtt",
|
|
66
|
+
"help": "MQTT broker WebSocket URL (e.g., ws://192.168.1.152:8083/mqtt)"
|
|
67
|
+
},
|
|
68
|
+
"mqttTopic": {
|
|
69
|
+
"label": "MQTT Topic",
|
|
70
|
+
"placeholder": "announcement/tester",
|
|
71
|
+
"help": "MQTT topic to subscribe/publish"
|
|
72
|
+
},
|
|
73
|
+
"groupPolicy": {
|
|
74
|
+
"label": "Group Policy",
|
|
75
|
+
"description": "Message policy for group chats"
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
},
|
|
7
80
|
"cli": ["admin-bridge"],
|
|
8
81
|
"configSchema": {
|
|
9
82
|
"type": "object",
|
package/package.json
CHANGED
|
@@ -107,6 +107,41 @@ describe('artifacts workspace scope', () => {
|
|
|
107
107
|
);
|
|
108
108
|
});
|
|
109
109
|
|
|
110
|
+
test('indexes only markdown files added after the initial baseline', async () => {
|
|
111
|
+
const context = await createMethodContext();
|
|
112
|
+
const workspaceRoot = path.join(context.openclawRoot, 'workspace');
|
|
113
|
+
|
|
114
|
+
await fs.mkdir(path.join(workspaceRoot, 'docs'), { recursive: true });
|
|
115
|
+
await fs.writeFile(path.join(workspaceRoot, 'docs', 'existing.md'), '# existing\n', 'utf8');
|
|
116
|
+
await fs.writeFile(path.join(workspaceRoot, 'MEMORY.md'), '# memory\n', 'utf8');
|
|
117
|
+
await fs.writeFile(path.join(workspaceRoot, 'SoUL.md'), '# soul\n', 'utf8');
|
|
118
|
+
|
|
119
|
+
const baseline = await refreshArtifacts({}, context) as {
|
|
120
|
+
count: number;
|
|
121
|
+
items: Array<{ relativePath: string }>;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
assert.equal(baseline.count, 0);
|
|
125
|
+
assert.deepEqual(baseline.items, []);
|
|
126
|
+
|
|
127
|
+
await fs.writeFile(path.join(workspaceRoot, 'docs', 'existing.md'), '# changed\n', 'utf8');
|
|
128
|
+
await fs.writeFile(path.join(workspaceRoot, 'MEMORY.md'), '# changed memory\n', 'utf8');
|
|
129
|
+
await fs.writeFile(path.join(workspaceRoot, 'docs', 'generated.md'), '# generated\n', 'utf8');
|
|
130
|
+
|
|
131
|
+
const refreshed = await refreshArtifacts({}, context) as {
|
|
132
|
+
count: number;
|
|
133
|
+
items: Array<{ relativePath: string; category: string; mimeType: string }>;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
assert.equal(refreshed.count, 1);
|
|
137
|
+
assert.deepEqual(
|
|
138
|
+
refreshed.items.map((item) => item.relativePath),
|
|
139
|
+
['docs/generated.md']
|
|
140
|
+
);
|
|
141
|
+
assert.equal(refreshed.items[0]?.category, 'document');
|
|
142
|
+
assert.equal(refreshed.items[0]?.mimeType, 'text/markdown');
|
|
143
|
+
});
|
|
144
|
+
|
|
110
145
|
test('list reads workspace manifest without taskId', async () => {
|
|
111
146
|
const context = await createMethodContext();
|
|
112
147
|
const workspaceRoot = path.join(context.openclawRoot, 'workspace');
|
|
@@ -40,6 +40,19 @@ interface ArtifactScanParams {
|
|
|
40
40
|
refresh?: boolean;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
interface MarkdownScanStateFile {
|
|
44
|
+
sizeBytes: number;
|
|
45
|
+
mtimeMs: number;
|
|
46
|
+
firstSeenAt: string;
|
|
47
|
+
baseline: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface MarkdownScanState {
|
|
51
|
+
initializedAt: string;
|
|
52
|
+
lastScanAt: string;
|
|
53
|
+
files: Record<string, MarkdownScanStateFile>;
|
|
54
|
+
}
|
|
55
|
+
|
|
43
56
|
interface ArtifactGetContentParams {
|
|
44
57
|
artifactId?: string;
|
|
45
58
|
relativePath?: string;
|
|
@@ -91,8 +104,19 @@ interface OpenClawConfig {
|
|
|
91
104
|
const INLINE_CONTENT_LIMIT_BYTES = 10 * 1024 * 1024;
|
|
92
105
|
const MIN_ARTIFACT_SIZE_BYTES = 1;
|
|
93
106
|
const ARTIFACT_MANIFEST_FILE = 'artifacts.json';
|
|
107
|
+
const MARKDOWN_SCAN_STATE_FILE = 'md-scan-state.json';
|
|
108
|
+
const PLUGIN_WORKSPACE_STATE_DIR = path.join('.openclaw', 'rol-websocket-channel');
|
|
94
109
|
const IGNORE_EXTENSIONS = new Set(['.tmp', '.part', '.crdownload']);
|
|
95
110
|
const IGNORE_FILE_NAMES = new Set(['.ds_store', 'thumbs.db', ARTIFACT_MANIFEST_FILE]);
|
|
111
|
+
const IGNORE_MARKDOWN_FILE_NAMES = new Set([
|
|
112
|
+
'agents.md',
|
|
113
|
+
'heartbeat.md',
|
|
114
|
+
'identity.md',
|
|
115
|
+
'memory.md',
|
|
116
|
+
'soul.md',
|
|
117
|
+
'tools.md',
|
|
118
|
+
'user.md'
|
|
119
|
+
]);
|
|
96
120
|
const IGNORE_DIRECTORY_NAMES = new Set([
|
|
97
121
|
'.git',
|
|
98
122
|
'.cache',
|
|
@@ -124,6 +148,7 @@ const CATEGORY_BY_EXTENSION: Record<string, ArtifactCategory> = {
|
|
|
124
148
|
'.pdf': 'document',
|
|
125
149
|
'.doc': 'document',
|
|
126
150
|
'.docx': 'document',
|
|
151
|
+
'.md': 'document',
|
|
127
152
|
'.zip': 'archive',
|
|
128
153
|
'.7z': 'archive',
|
|
129
154
|
'.tar': 'archive',
|
|
@@ -147,6 +172,7 @@ const MIME_BY_EXTENSION: Record<string, string> = {
|
|
|
147
172
|
'.pdf': 'application/pdf',
|
|
148
173
|
'.doc': 'application/msword',
|
|
149
174
|
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
175
|
+
'.md': 'text/markdown',
|
|
150
176
|
'.zip': 'application/zip',
|
|
151
177
|
'.7z': 'application/x-7z-compressed',
|
|
152
178
|
'.tar': 'application/x-tar',
|
|
@@ -157,6 +183,7 @@ const MIME_BY_EXTENSION: Record<string, string> = {
|
|
|
157
183
|
interface WorkspacePaths {
|
|
158
184
|
workspaceRoot: string;
|
|
159
185
|
manifestPath: string;
|
|
186
|
+
markdownScanStatePath: string;
|
|
160
187
|
}
|
|
161
188
|
|
|
162
189
|
interface PresignedPostUploadTarget {
|
|
@@ -411,6 +438,9 @@ async function refreshArtifactManifest(
|
|
|
411
438
|
|
|
412
439
|
const existing = await readExistingManifest(manifestPath);
|
|
413
440
|
const existingByPath = new Map(existing.map((item) => [item.relativePath, item]));
|
|
441
|
+
const loadedMarkdownScanState = await readMarkdownScanState(workspacePaths.markdownScanStatePath);
|
|
442
|
+
const hadMarkdownScanState = loadedMarkdownScanState !== null;
|
|
443
|
+
const markdownScanState = loadedMarkdownScanState ?? createMarkdownScanState();
|
|
414
444
|
const files = await collectArtifactFiles(workspaceRoot);
|
|
415
445
|
const items: ArtifactRecord[] = [];
|
|
416
446
|
|
|
@@ -423,7 +453,18 @@ async function refreshArtifactManifest(
|
|
|
423
453
|
const relativePath = path.relative(workspaceRoot, fullPath).replace(/\\/g, '/');
|
|
424
454
|
const existingItem = existingByPath.get(relativePath);
|
|
425
455
|
const fileName = path.basename(fullPath);
|
|
426
|
-
|
|
456
|
+
const isMarkdown = isMarkdownArtifactFile(fileName);
|
|
457
|
+
if (isMarkdown) {
|
|
458
|
+
if (!shouldIncludeMarkdownArtifactFile(
|
|
459
|
+
relativePath,
|
|
460
|
+
fileName,
|
|
461
|
+
stat,
|
|
462
|
+
markdownScanState,
|
|
463
|
+
hadMarkdownScanState
|
|
464
|
+
)) {
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
} else if (!shouldIncludeArtifactFile(fileName)) {
|
|
427
468
|
continue;
|
|
428
469
|
}
|
|
429
470
|
|
|
@@ -453,6 +494,8 @@ async function refreshArtifactManifest(
|
|
|
453
494
|
}
|
|
454
495
|
|
|
455
496
|
items.sort((a, b) => a.fileName.localeCompare(b.fileName));
|
|
497
|
+
markdownScanState.lastScanAt = new Date().toISOString();
|
|
498
|
+
await writeMarkdownScanState(workspacePaths.markdownScanStatePath, markdownScanState);
|
|
456
499
|
await writeArtifactManifest(manifestPath, items);
|
|
457
500
|
|
|
458
501
|
return { manifestPath, items, workspacePaths };
|
|
@@ -499,6 +542,37 @@ async function writeArtifactManifest(manifestPath: string, items: ArtifactRecord
|
|
|
499
542
|
);
|
|
500
543
|
}
|
|
501
544
|
|
|
545
|
+
async function readMarkdownScanState(statePath: string): Promise<MarkdownScanState | null> {
|
|
546
|
+
if (!(await pathExists(statePath))) {
|
|
547
|
+
return null;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const parsed = await readJsonFile<JsonValue>(statePath);
|
|
551
|
+
if (!isObject(parsed)) {
|
|
552
|
+
return null;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const files: Record<string, MarkdownScanStateFile> = {};
|
|
556
|
+
if (isObject(parsed.files)) {
|
|
557
|
+
for (const [relativePath, value] of Object.entries(parsed.files)) {
|
|
558
|
+
if (isMarkdownScanStateFile(value)) {
|
|
559
|
+
files[relativePath] = value;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
return {
|
|
565
|
+
initializedAt: typeof parsed.initializedAt === 'string' ? parsed.initializedAt : new Date().toISOString(),
|
|
566
|
+
lastScanAt: typeof parsed.lastScanAt === 'string' ? parsed.lastScanAt : new Date().toISOString(),
|
|
567
|
+
files
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
async function writeMarkdownScanState(statePath: string, state: MarkdownScanState): Promise<void> {
|
|
572
|
+
await ensureDir(path.dirname(statePath));
|
|
573
|
+
await fs.writeFile(statePath, JSON.stringify(state, null, 2), 'utf8');
|
|
574
|
+
}
|
|
575
|
+
|
|
502
576
|
async function persistUploadedArtifact(
|
|
503
577
|
manifestPath: string,
|
|
504
578
|
items: ArtifactRecord[],
|
|
@@ -625,12 +699,26 @@ function resolveWorkspaceRoot(openclawRoot: string): string {
|
|
|
625
699
|
async function ensureWorkspacePaths(openclawRoot: string): Promise<WorkspacePaths> {
|
|
626
700
|
const workspaceRoot = resolveWorkspaceRoot(openclawRoot);
|
|
627
701
|
const manifestPath = ensureInside(workspaceRoot, path.join(workspaceRoot, ARTIFACT_MANIFEST_FILE));
|
|
702
|
+
const markdownScanStatePath = ensureInside(
|
|
703
|
+
workspaceRoot,
|
|
704
|
+
path.join(workspaceRoot, PLUGIN_WORKSPACE_STATE_DIR, MARKDOWN_SCAN_STATE_FILE)
|
|
705
|
+
);
|
|
628
706
|
|
|
629
707
|
await ensureDir(workspaceRoot);
|
|
630
708
|
|
|
631
709
|
return {
|
|
632
710
|
workspaceRoot,
|
|
633
|
-
manifestPath
|
|
711
|
+
manifestPath,
|
|
712
|
+
markdownScanStatePath
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function createMarkdownScanState(): MarkdownScanState {
|
|
717
|
+
const now = new Date().toISOString();
|
|
718
|
+
return {
|
|
719
|
+
initializedAt: now,
|
|
720
|
+
lastScanAt: now,
|
|
721
|
+
files: {}
|
|
634
722
|
};
|
|
635
723
|
}
|
|
636
724
|
|
|
@@ -643,6 +731,10 @@ function normalizeExtension(fileName: string): string | null {
|
|
|
643
731
|
return ext || null;
|
|
644
732
|
}
|
|
645
733
|
|
|
734
|
+
function isMarkdownArtifactFile(fileName: string): boolean {
|
|
735
|
+
return path.extname(fileName).toLowerCase() === '.md';
|
|
736
|
+
}
|
|
737
|
+
|
|
646
738
|
function normalizeRelativePath(value: string | null): string | null {
|
|
647
739
|
if (!value) {
|
|
648
740
|
return null;
|
|
@@ -687,6 +779,52 @@ function shouldIncludeArtifactFile(fileName: string): boolean {
|
|
|
687
779
|
return classifyArtifactCategory(fileName) !== 'other';
|
|
688
780
|
}
|
|
689
781
|
|
|
782
|
+
function shouldIncludeMarkdownArtifactFile(
|
|
783
|
+
relativePath: string,
|
|
784
|
+
fileName: string,
|
|
785
|
+
stat: { size: number; mtimeMs: number },
|
|
786
|
+
state: MarkdownScanState,
|
|
787
|
+
hadState: boolean
|
|
788
|
+
): boolean {
|
|
789
|
+
if (shouldIgnoreArtifactFile(fileName)) {
|
|
790
|
+
return false;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
if (IGNORE_MARKDOWN_FILE_NAMES.has(fileName.trim().toLowerCase())) {
|
|
794
|
+
return false;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
const existing = state.files[relativePath];
|
|
798
|
+
if (existing) {
|
|
799
|
+
existing.sizeBytes = stat.size;
|
|
800
|
+
existing.mtimeMs = stat.mtimeMs;
|
|
801
|
+
return !existing.baseline;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
state.files[relativePath] = {
|
|
805
|
+
sizeBytes: stat.size,
|
|
806
|
+
mtimeMs: stat.mtimeMs,
|
|
807
|
+
firstSeenAt: new Date().toISOString(),
|
|
808
|
+
baseline: !hadState
|
|
809
|
+
};
|
|
810
|
+
|
|
811
|
+
return hadState;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function isMarkdownScanStateFile(value: unknown): value is MarkdownScanStateFile {
|
|
815
|
+
if (!value || Array.isArray(value) || typeof value !== 'object') {
|
|
816
|
+
return false;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
const objectValue = value as Record<string, unknown>;
|
|
820
|
+
return (
|
|
821
|
+
typeof objectValue.sizeBytes === 'number' &&
|
|
822
|
+
typeof objectValue.mtimeMs === 'number' &&
|
|
823
|
+
typeof objectValue.firstSeenAt === 'string' &&
|
|
824
|
+
typeof objectValue.baseline === 'boolean'
|
|
825
|
+
);
|
|
826
|
+
}
|
|
827
|
+
|
|
690
828
|
function isArtifactRecord(value: unknown): value is ArtifactRecord {
|
|
691
829
|
if (!value || Array.isArray(value) || typeof value !== 'object') {
|
|
692
830
|
return false;
|
|
@@ -48,7 +48,7 @@ import {
|
|
|
48
48
|
updateSkillFromClawHub
|
|
49
49
|
} from './skills.js';
|
|
50
50
|
import { getInstalledSkill, uninstallSkill } from './skills-extended.js';
|
|
51
|
-
import {
|
|
51
|
+
import { doctorFix, logs, openclawUpdate, ping, pluginSelfUpdate, restart, stop } from './system.js';
|
|
52
52
|
import {
|
|
53
53
|
getUsageBreakdown,
|
|
54
54
|
getUsagePageSummary,
|
|
@@ -63,6 +63,8 @@ const methods = new Map<string, MethodHandler>([
|
|
|
63
63
|
['system.stop', stop],
|
|
64
64
|
['system.doctorFix', doctorFix],
|
|
65
65
|
['system.logs', logs],
|
|
66
|
+
['system.openclawUpdate', openclawUpdate],
|
|
67
|
+
['system.pluginSelfUpdate', pluginSelfUpdate],
|
|
66
68
|
|
|
67
69
|
// Agents
|
|
68
70
|
['agents.get', getAgents],
|
|
@@ -0,0 +1,39 @@
|
|
|
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
|
+
|
|
7
|
+
import { findMem9RuntimeEntrypoint } from './mem9.js';
|
|
8
|
+
|
|
9
|
+
describe('mem9 runtime compatibility', () => {
|
|
10
|
+
test('accepts compiled runtime output under the installed package', async () => {
|
|
11
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'mem9-runtime-'));
|
|
12
|
+
try {
|
|
13
|
+
const packageRoot = path.join(root, '.openclaw', 'npm', 'node_modules', '@mem9', 'mem9');
|
|
14
|
+
await fs.mkdir(path.join(packageRoot, 'dist'), { recursive: true });
|
|
15
|
+
await fs.writeFile(path.join(packageRoot, 'dist', 'index.js'), 'export default {};\n', 'utf8');
|
|
16
|
+
|
|
17
|
+
const entrypoint = await findMem9RuntimeEntrypoint(path.join(root, '.openclaw'));
|
|
18
|
+
|
|
19
|
+
assert.equal(entrypoint, path.join(packageRoot, 'dist', 'index.js'));
|
|
20
|
+
} finally {
|
|
21
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('rejects TypeScript-only mem9 packages before writing memory slot', async () => {
|
|
26
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'mem9-runtime-'));
|
|
27
|
+
try {
|
|
28
|
+
const packageRoot = path.join(root, '.openclaw', 'npm', 'node_modules', '@mem9', 'mem9');
|
|
29
|
+
await fs.mkdir(packageRoot, { recursive: true });
|
|
30
|
+
await fs.writeFile(path.join(packageRoot, 'index.ts'), 'export default {};\n', 'utf8');
|
|
31
|
+
|
|
32
|
+
const entrypoint = await findMem9RuntimeEntrypoint(path.join(root, '.openclaw'));
|
|
33
|
+
|
|
34
|
+
assert.equal(entrypoint, null);
|
|
35
|
+
} finally {
|
|
36
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -13,6 +13,18 @@ const MEM9_PLUGIN_ID = 'mem9';
|
|
|
13
13
|
const MEM9_API_URL = 'https://api.mem9.ai';
|
|
14
14
|
const MEM9_CREATE_URL = `${MEM9_API_URL}/v1alpha1/mem9s`;
|
|
15
15
|
const GATEWAY_SERVICE = 'openclaw-gateway.service';
|
|
16
|
+
const MEM9_PACKAGE_ROOTS = [
|
|
17
|
+
path.join('npm', 'node_modules', '@mem9', 'mem9'),
|
|
18
|
+
path.join('npm', 'node_modules', 'mem9')
|
|
19
|
+
];
|
|
20
|
+
const RUNTIME_ENTRYPOINTS = [
|
|
21
|
+
path.join('dist', 'index.js'),
|
|
22
|
+
path.join('dist', 'index.mjs'),
|
|
23
|
+
path.join('dist', 'index.cjs'),
|
|
24
|
+
'index.js',
|
|
25
|
+
'index.mjs',
|
|
26
|
+
'index.cjs'
|
|
27
|
+
];
|
|
16
28
|
|
|
17
29
|
interface OpenClawConfig {
|
|
18
30
|
plugins?: {
|
|
@@ -30,9 +42,11 @@ export async function installMem9(context: MethodContext): Promise<JsonValue> {
|
|
|
30
42
|
await ensureNodeRuntime();
|
|
31
43
|
|
|
32
44
|
const currentState = readMem9State(config);
|
|
33
|
-
const
|
|
45
|
+
const currentEntrypoint = await findMem9RuntimeEntrypoint(context.openclawRoot);
|
|
46
|
+
const installResult = currentState.installed && currentEntrypoint
|
|
34
47
|
? { attempted: false, installed: true }
|
|
35
48
|
: await installMem9Plugin(context.projectRoot);
|
|
49
|
+
const runtimeEntrypoint = await ensureMem9RuntimeEntrypoint(context.openclawRoot);
|
|
36
50
|
|
|
37
51
|
if (currentState.configured && currentState.apiKey) {
|
|
38
52
|
const updated = await ensureMem9SlotConfig(context.openclawRoot, currentState.apiKey);
|
|
@@ -46,6 +60,7 @@ export async function installMem9(context: MethodContext): Promise<JsonValue> {
|
|
|
46
60
|
createdNewKey: false,
|
|
47
61
|
reusedExistingKey: true,
|
|
48
62
|
plugin: MEM9_PLUGIN_ID,
|
|
63
|
+
runtimeEntrypoint,
|
|
49
64
|
apiUrl: MEM9_API_URL,
|
|
50
65
|
apiKey: currentState.apiKey,
|
|
51
66
|
updated,
|
|
@@ -65,6 +80,7 @@ export async function installMem9(context: MethodContext): Promise<JsonValue> {
|
|
|
65
80
|
createdNewKey: true,
|
|
66
81
|
reusedExistingKey: false,
|
|
67
82
|
plugin: MEM9_PLUGIN_ID,
|
|
83
|
+
runtimeEntrypoint,
|
|
68
84
|
apiUrl: MEM9_API_URL,
|
|
69
85
|
apiKey,
|
|
70
86
|
updated,
|
|
@@ -80,6 +96,7 @@ export async function reconnectMem9(key: string, context: MethodContext): Promis
|
|
|
80
96
|
|
|
81
97
|
const config = await ensureOpenClawConfigExists(context.openclawRoot);
|
|
82
98
|
const previousState = readMem9State(config);
|
|
99
|
+
const runtimeEntrypoint = await ensureMem9RuntimeEntrypoint(context.openclawRoot);
|
|
83
100
|
const updated = await writeMem9Config(context.openclawRoot, apiKey);
|
|
84
101
|
const restart = await restartGateway(context.projectRoot);
|
|
85
102
|
|
|
@@ -88,6 +105,7 @@ export async function reconnectMem9(key: string, context: MethodContext): Promis
|
|
|
88
105
|
reconnected: true,
|
|
89
106
|
replacedExistingKey: Boolean(previousState.apiKey && previousState.apiKey !== apiKey),
|
|
90
107
|
plugin: MEM9_PLUGIN_ID,
|
|
108
|
+
runtimeEntrypoint,
|
|
91
109
|
apiUrl: MEM9_API_URL,
|
|
92
110
|
apiKey,
|
|
93
111
|
updated: ['plugins.entries.mem9.config.apiKey', ...updated.filter((item) => item !== 'plugins.entries.mem9' && item !== 'plugins.slots.memory')],
|
|
@@ -171,6 +189,35 @@ async function installMem9Plugin(cwd: string): Promise<{ attempted: boolean; ins
|
|
|
171
189
|
};
|
|
172
190
|
}
|
|
173
191
|
|
|
192
|
+
export async function findMem9RuntimeEntrypoint(openclawRoot: string): Promise<string | null> {
|
|
193
|
+
for (const packageRoot of MEM9_PACKAGE_ROOTS.map((item) => path.join(openclawRoot, item))) {
|
|
194
|
+
for (const entrypoint of RUNTIME_ENTRYPOINTS.map((item) => path.join(packageRoot, item))) {
|
|
195
|
+
if (await pathExists(entrypoint)) {
|
|
196
|
+
return entrypoint;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function ensureMem9RuntimeEntrypoint(openclawRoot: string): Promise<string> {
|
|
205
|
+
const entrypoint = await findMem9RuntimeEntrypoint(openclawRoot);
|
|
206
|
+
if (entrypoint) {
|
|
207
|
+
return entrypoint;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
throw new JsonRpcException(
|
|
211
|
+
JSON_RPC_ERRORS.internalError,
|
|
212
|
+
'mem9 plugin is installed but missing compiled runtime output required by OpenClaw 2026.5.6',
|
|
213
|
+
{
|
|
214
|
+
code: 'MEM9_RUNTIME_OUTPUT_MISSING',
|
|
215
|
+
expected: RUNTIME_ENTRYPOINTS.map((item) => `./${item.replace(/\\/g, '/')}`),
|
|
216
|
+
packageRoots: MEM9_PACKAGE_ROOTS.map((item) => path.join(openclawRoot, item))
|
|
217
|
+
}
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
174
221
|
async function createMem9Key(): Promise<string> {
|
|
175
222
|
const response = await fetch(MEM9_CREATE_URL, {
|
|
176
223
|
method: 'POST',
|