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.
@@ -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
  }
@@ -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 { restart, stop, doctorFix, logs } from './src/admin/methods/system.js';
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
- if (!shouldIncludeArtifactFile(fileName)) {
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 { ping, restart, stop, doctorFix, logs } from './system.js';
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 installResult = currentState.installed
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
+ }