rol-websocket-channel 1.6.2 → 1.6.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.
package/dist/index.js CHANGED
@@ -490,7 +490,8 @@ async function handleIncomingMessage(payload, account, cfg, runtime, log, mqttTo
490
490
  log?.error(`[rol-websocket-channel] Failed to process message: ${err instanceof Error ? err.message : String(err)}`);
491
491
  }
492
492
  }
493
- async function handleCustomMessageType(msgType, innerData, traceId, accountId, mqttTopic) {
493
+ const immediateAckMessageTypes = new Set(["openclawUpdate", "pluginSelfUpdate"]);
494
+ export async function handleCustomMessageType(msgType, innerData, traceId, accountId, mqttTopic) {
494
495
  const isSkillInstallFlow = msgType === "skillsInstallFromClawHub" || msgType === "skillsUpdateFromClawHub";
495
496
  const response = {
496
497
  type: "receiver",
@@ -503,6 +504,19 @@ async function handleCustomMessageType(msgType, innerData, traceId, accountId, m
503
504
  }
504
505
  const handlerMethod = messageHandler[msgType];
505
506
  if (typeof handlerMethod === "function") {
507
+ if (immediateAckMessageTypes.has(msgType)) {
508
+ response.success = true;
509
+ response.data = {
510
+ ok: true,
511
+ action: msgType,
512
+ status: "running",
513
+ accepted: true,
514
+ message: `${msgType} started`,
515
+ };
516
+ publishCustomMessageResponse(response, innerData, mqttTopic);
517
+ void runCustomMessageHandler(msgType, handlerMethod, innerData, response, mqttTopic);
518
+ return;
519
+ }
506
520
  try {
507
521
  const methodResult = await handlerMethod.call(messageHandler, innerData);
508
522
  // 兼容两种返回格式:
@@ -516,6 +530,12 @@ async function handleCustomMessageType(msgType, innerData, traceId, accountId, m
516
530
  response.data = methodResult.result;
517
531
  if (!methodResult.ok) {
518
532
  response.error = methodResult.error?.message || "Unknown error";
533
+ if (methodResult.error?.code !== undefined) {
534
+ response.error_code = methodResult.error.code;
535
+ }
536
+ if (methodResult.error?.data !== undefined) {
537
+ response.error_data = methodResult.error.data;
538
+ }
519
539
  if (isSkillInstallFlow) {
520
540
  console.error(`[rol-websocket-channel] custom message failed: type=${msgType}, traceId=${traceId}, slug=${innerData?.slug ?? ""}, error=${response.error}, detail=${JSON.stringify(methodResult.error?.data ?? {})}`);
521
541
  }
@@ -545,6 +565,41 @@ async function handleCustomMessageType(msgType, innerData, traceId, accountId, m
545
565
  response.success = false;
546
566
  response.error = `Unknown message type: ${msgType}`;
547
567
  }
568
+ publishCustomMessageResponse(response, innerData, mqttTopic);
569
+ }
570
+ async function runCustomMessageHandler(msgType, handlerMethod, innerData, baseResponse, mqttTopic) {
571
+ const response = {
572
+ ...baseResponse,
573
+ timestamp: Date.now(),
574
+ };
575
+ try {
576
+ const methodResult = await handlerMethod.call(messageHandler, innerData);
577
+ if (typeof methodResult === "object" &&
578
+ methodResult !== null &&
579
+ "ok" in methodResult) {
580
+ response.success = methodResult.ok;
581
+ response.data = methodResult.result;
582
+ if (!methodResult.ok) {
583
+ response.error = methodResult.error?.message || "Unknown error";
584
+ }
585
+ else {
586
+ delete response.error;
587
+ }
588
+ }
589
+ else {
590
+ response.success = true;
591
+ response.data = methodResult;
592
+ delete response.error;
593
+ }
594
+ }
595
+ catch (handlerErr) {
596
+ response.success = false;
597
+ response.data = undefined;
598
+ response.error = handlerErr.message;
599
+ }
600
+ publishCustomMessageResponse(response, innerData, mqttTopic);
601
+ }
602
+ function publishCustomMessageResponse(response, innerData, mqttTopic) {
548
603
  const conn = ConnectionManager.getGlobalConnection();
549
604
  if (conn && conn.ws && conn.ws.connected) {
550
605
  // 根据 source_type 修改 topic 末尾的 #
@@ -6,7 +6,7 @@ import { getContext } from './src/shared/context.js';
6
6
  import { wrapAdminCall } from './src/shared/wrapper.js';
7
7
  import { getAgents, getConfig, setApiCoreBotConfig } from './src/admin/methods/admin.js';
8
8
  import { createAgent, deleteAgent, listAgents, updateAgent } from './src/admin/methods/agents-extended.js';
9
- import { createArtifactRecord, ensureArtifactUploaded, getArtifactContent, getArtifactPresignedPost, listArtifacts, markArtifactUploaded, refreshArtifacts } from './src/admin/methods/artifacts.js';
9
+ import { createArtifactRecord, ensureArtifactUploaded, findLatestArtifacts, getArtifactContent, getArtifactPresignedPost, listArtifacts, markArtifactUploaded, publishArtifact, refreshArtifacts } from './src/admin/methods/artifacts.js';
10
10
  import { addCron, disableCron, enableCron, getCronStatus, listCron, listCronRuns, renameCron, removeCron, rescheduleCron, runCron, setCronContent, updateCronMessage, updateCronSystemEvent } from './src/admin/methods/cron.js';
11
11
  import { listSessions } from './src/admin/methods/sessions.js';
12
12
  import { getSession, prepareMessage, attachSkill } from './src/admin/methods/sessions-extended.js';
@@ -400,6 +400,12 @@ export class MessageHandler {
400
400
  return await refreshArtifacts(data, context);
401
401
  });
402
402
  }
403
+ async artifactsFindLatest(data) {
404
+ return wrapAdminCall(async () => {
405
+ const context = getContext();
406
+ return await findLatestArtifacts(data, context);
407
+ });
408
+ }
403
409
  async artifactsGetContent(data) {
404
410
  return wrapAdminCall(async () => {
405
411
  const context = getContext();
@@ -412,6 +418,12 @@ export class MessageHandler {
412
418
  return await ensureArtifactUploaded(data, context);
413
419
  });
414
420
  }
421
+ async artifactsPublish(data) {
422
+ return wrapAdminCall(async () => {
423
+ const context = getContext();
424
+ return await publishArtifact(data, context);
425
+ });
426
+ }
415
427
  async artifactsGetPresignedPost(data) {
416
428
  return wrapAdminCall(async () => {
417
429
  const context = getContext();
@@ -2,8 +2,6 @@ import path from 'node:path';
2
2
  import { readJsonFile, writeJsonFile } from '../lib/fs.js';
3
3
  import { JsonRpcException, JSON_RPC_ERRORS } from '../jsonrpc.js';
4
4
  const DEFAULT_PLUGIN_ID = 'rol-websocket-channel';
5
- const DEFAULT_API_CORE_BOT_BASE_URL = 'http://192.168.1.23:9092';
6
- const DEFAULT_API_CORE_BOT_AUTH_TOKEN = '123';
7
5
  export const getAgents = async (_params, context) => {
8
6
  const configPath = path.join(context.openclawRoot, 'openclaw.json');
9
7
  const config = await readJsonFile(configPath);
@@ -36,10 +34,10 @@ export const setApiCoreBotConfig = async (params, context) => {
36
34
  const objectParams = isObject(params) ? params : {};
37
35
  const baseUrl = typeof objectParams.baseUrl === 'string'
38
36
  ? objectParams.baseUrl.trim().replace(/\/+$/, '')
39
- : DEFAULT_API_CORE_BOT_BASE_URL;
37
+ : '';
40
38
  const authToken = typeof objectParams.authToken === 'string' && objectParams.authToken.trim()
41
39
  ? objectParams.authToken.trim()
42
- : DEFAULT_API_CORE_BOT_AUTH_TOKEN;
40
+ : null;
43
41
  if (!baseUrl) {
44
42
  throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, 'apiCoreBot.baseUrl is required');
45
43
  }
@@ -43,15 +43,12 @@ test('setApiCoreBotConfig writes apiCoreBot endpoint without changing pairing or
43
43
  assert.equal(savedConfig.plugins.entries['rol-websocket-channel'].config.pairing.pairingKeyLast4, '7a46');
44
44
  assert.equal(savedConfig.channels['rol-websocket-channel'].config.mqttUrl, 'ws://mqtt.example.test:8083/mqtt');
45
45
  });
46
- test('setApiCoreBotConfig uses default apiCoreBot values when omitted', async () => {
46
+ test('setApiCoreBotConfig rejects missing baseUrl instead of writing defaults', async () => {
47
47
  const context = await createMethodContext();
48
- await fs.writeFile(path.join(context.openclawRoot, 'openclaw.json'), '{}');
49
- const result = await setApiCoreBotConfig({}, context);
50
- const savedConfig = JSON.parse(await fs.readFile(path.join(context.openclawRoot, 'openclaw.json'), 'utf8'));
51
- assert.equal(result.apiCoreBot.baseUrl, 'http://192.168.1.23:9092');
52
- assert.equal(result.apiCoreBot.authToken, '********');
53
- assert.equal(savedConfig.plugins.entries['rol-websocket-channel'].config.apiCoreBot.baseUrl, 'http://192.168.1.23:9092');
54
- assert.equal(savedConfig.plugins.entries['rol-websocket-channel'].config.apiCoreBot.authToken, '123');
48
+ const configPath = path.join(context.openclawRoot, 'openclaw.json');
49
+ await fs.writeFile(configPath, '{}');
50
+ await assert.rejects(() => setApiCoreBotConfig({}, context), /apiCoreBot\.baseUrl is required/);
51
+ assert.equal(await fs.readFile(configPath, 'utf8'), '{}');
55
52
  });
56
53
  async function createMethodContext() {
57
54
  const projectRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'api-core-bot-config-test-'));
@@ -34,7 +34,7 @@ function resolveOpenClawBin() {
34
34
  export async function installMem9(context) {
35
35
  const config = await ensureOpenClawConfigExists(context.openclawRoot);
36
36
  const currentState = readMem9State(config);
37
- const currentEntrypoint = await findMem9RuntimeEntrypoint(context.openclawRoot);
37
+ const currentEntrypoint = await findMem9RuntimeEntrypoint(context.openclawRoot, config);
38
38
  // Phase A: Plugin not installed → install only, then restart
39
39
  if (!currentState.installed && !currentEntrypoint) {
40
40
  await ensureOpenClawCli();
@@ -52,7 +52,7 @@ export async function installMem9(context) {
52
52
  }
53
53
  // Phase B: Installed but no key → create key, write config, restart
54
54
  if (!currentState.configured || !currentState.apiKey) {
55
- const runtimeEntrypoint = await ensureMem9RuntimeEntrypoint(context.openclawRoot);
55
+ const runtimeEntrypoint = await ensureMem9RuntimeEntrypoint(context.openclawRoot, config);
56
56
  const apiKey = await createMem9Key();
57
57
  const updated = await writeMem9Config(context.openclawRoot, apiKey);
58
58
  const restart = await restartGateway(context.projectRoot);
@@ -73,7 +73,7 @@ export async function installMem9(context) {
73
73
  };
74
74
  }
75
75
  // Phase C: Already configured → ensure slot/hooks/allow are correct
76
- const runtimeEntrypoint = await ensureMem9RuntimeEntrypoint(context.openclawRoot);
76
+ const runtimeEntrypoint = await ensureMem9RuntimeEntrypoint(context.openclawRoot, config);
77
77
  const updated = await ensureMem9SlotConfig(context.openclawRoot, currentState.apiKey);
78
78
  const restart = await restartGateway(context.projectRoot);
79
79
  return {
@@ -102,7 +102,7 @@ export async function reconnectMem9(key, context) {
102
102
  }
103
103
  const config = await ensureOpenClawConfigExists(context.openclawRoot);
104
104
  const previousState = readMem9State(config);
105
- const runtimeEntrypoint = await ensureMem9RuntimeEntrypoint(context.openclawRoot);
105
+ const runtimeEntrypoint = await ensureMem9RuntimeEntrypoint(context.openclawRoot, config);
106
106
  const updated = await writeMem9Config(context.openclawRoot, apiKey);
107
107
  const restart = await restartGateway(context.projectRoot);
108
108
  return {
@@ -178,8 +178,8 @@ async function installMem9Plugin(cwd) {
178
178
  });
179
179
  }
180
180
  }
181
- export async function findMem9RuntimeEntrypoint(openclawRoot) {
182
- for (const packageRoot of MEM9_PACKAGE_ROOTS.map((item) => path.join(openclawRoot, item))) {
181
+ export async function findMem9RuntimeEntrypoint(openclawRoot, config) {
182
+ for (const packageRoot of resolveMem9RuntimePackageRoots(openclawRoot, config)) {
183
183
  for (const entrypoint of RUNTIME_ENTRYPOINTS.map((item) => path.join(packageRoot, item))) {
184
184
  if (await pathExists(entrypoint)) {
185
185
  return entrypoint;
@@ -188,17 +188,34 @@ export async function findMem9RuntimeEntrypoint(openclawRoot) {
188
188
  }
189
189
  return null;
190
190
  }
191
- async function ensureMem9RuntimeEntrypoint(openclawRoot) {
192
- const entrypoint = await findMem9RuntimeEntrypoint(openclawRoot);
191
+ async function ensureMem9RuntimeEntrypoint(openclawRoot, config) {
192
+ const entrypoint = await findMem9RuntimeEntrypoint(openclawRoot, config);
193
193
  if (entrypoint) {
194
194
  return entrypoint;
195
195
  }
196
+ const installRecord = readMem9InstallRecord(config);
197
+ const checkedPackageRoots = resolveMem9RuntimePackageRoots(openclawRoot, config);
196
198
  throw new JsonRpcException(JSON_RPC_ERRORS.internalError, 'mem9 plugin is installed but missing compiled runtime output', {
197
199
  code: 'MEM9_RUNTIME_OUTPUT_MISSING',
198
200
  expected: RUNTIME_ENTRYPOINTS.map((item) => `./${item.replace(/\\/g, '/')}`),
199
- packageRoots: MEM9_PACKAGE_ROOTS.map((item) => path.join(openclawRoot, item))
201
+ checkedPackageRoots,
202
+ checkedEntrypoints: checkedPackageRoots.flatMap((packageRoot) => RUNTIME_ENTRYPOINTS.map((item) => path.join(packageRoot, item))),
203
+ installRecord
200
204
  });
201
205
  }
206
+ function resolveMem9RuntimePackageRoots(openclawRoot, config) {
207
+ const roots = [];
208
+ const installPath = pickString(readMem9InstallRecord(config)?.installPath);
209
+ if (installPath) {
210
+ roots.push(path.isAbsolute(installPath) ? installPath : path.resolve(openclawRoot, installPath));
211
+ }
212
+ for (const fallbackRoot of MEM9_PACKAGE_ROOTS.map((item) => path.join(openclawRoot, item))) {
213
+ if (!roots.includes(fallbackRoot)) {
214
+ roots.push(fallbackRoot);
215
+ }
216
+ }
217
+ return roots;
218
+ }
202
219
  async function createMem9Key() {
203
220
  let response;
204
221
  try {
@@ -368,6 +385,10 @@ function pickString(value) {
368
385
  function isRecord(value) {
369
386
  return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
370
387
  }
388
+ function readMem9InstallRecord(config) {
389
+ const record = config?.plugins?.installs?.[MEM9_PLUGIN_ID];
390
+ return isRecord(record) ? record : null;
391
+ }
371
392
  function readMem9State(config) {
372
393
  const installed = Boolean((config.plugins?.installs && typeof config.plugins.installs === 'object' && MEM9_PLUGIN_ID in config.plugins.installs)
373
394
  || (config.plugins?.entries && typeof config.plugins.entries === 'object' && MEM9_PLUGIN_ID in config.plugins.entries));
@@ -32,6 +32,16 @@ export const updateModels = async (params, context) => {
32
32
  const changes = [];
33
33
  // 更新 primary model
34
34
  if (primaryModel) {
35
+ const inferredProvider = inferProviderFromPrimaryModel(primaryModel);
36
+ if (!inferredProvider) {
37
+ throwModelError('MODEL_PRIMARY_INVALID', 'primaryModel must be in provider/model format');
38
+ }
39
+ if (modelProvider && modelProvider !== inferredProvider) {
40
+ throwModelError('MODEL_PROVIDER_MISMATCH', 'modelProvider does not match primaryModel');
41
+ }
42
+ if (!isAllowedModel(config, primaryModel)) {
43
+ throwModelError('MODEL_NOT_ALLOWED', 'model is not in allowed model options');
44
+ }
35
45
  if (!config.agents)
36
46
  config.agents = {};
37
47
  if (!config.agents.defaults)
@@ -39,10 +49,6 @@ export const updateModels = async (params, context) => {
39
49
  if (!config.agents.defaults.model)
40
50
  config.agents.defaults.model = {};
41
51
  config.agents.defaults.model.primary = primaryModel;
42
- const inferredProvider = inferProviderFromPrimaryModel(primaryModel);
43
- if (modelProvider && modelProvider !== inferredProvider) {
44
- throwModelError('MODEL_PROVIDER_MISMATCH', 'modelProvider does not match primaryModel');
45
- }
46
52
  updated = true;
47
53
  changes.push(`Updated primary model to: ${primaryModel}`);
48
54
  }
package/index.ts CHANGED
@@ -625,7 +625,9 @@ async function handleIncomingMessage(
625
625
  }
626
626
  }
627
627
 
628
- async function handleCustomMessageType(
628
+ const immediateAckMessageTypes = new Set(["openclawUpdate", "pluginSelfUpdate"]);
629
+
630
+ export async function handleCustomMessageType(
629
631
  msgType: string,
630
632
  innerData: any,
631
633
  traceId: string,
@@ -648,6 +650,21 @@ async function handleCustomMessageType(
648
650
 
649
651
  const handlerMethod = (messageHandler as any)[msgType];
650
652
  if (typeof handlerMethod === "function") {
653
+ if (immediateAckMessageTypes.has(msgType)) {
654
+ response.success = true;
655
+ response.data = {
656
+ ok: true,
657
+ action: msgType,
658
+ status: "running",
659
+ accepted: true,
660
+ message: `${msgType} started`,
661
+ };
662
+ publishCustomMessageResponse(response, innerData, mqttTopic);
663
+
664
+ void runCustomMessageHandler(msgType, handlerMethod, innerData, response, mqttTopic);
665
+ return;
666
+ }
667
+
651
668
  try {
652
669
  const methodResult = await handlerMethod.call(messageHandler, innerData);
653
670
 
@@ -664,6 +681,12 @@ async function handleCustomMessageType(
664
681
  response.data = methodResult.result;
665
682
  if (!methodResult.ok) {
666
683
  response.error = methodResult.error?.message || "Unknown error";
684
+ if (methodResult.error?.code !== undefined) {
685
+ response.error_code = methodResult.error.code;
686
+ }
687
+ if (methodResult.error?.data !== undefined) {
688
+ response.error_data = methodResult.error.data;
689
+ }
667
690
  if (isSkillInstallFlow) {
668
691
  console.error(
669
692
  `[rol-websocket-channel] custom message failed: type=${msgType}, traceId=${traceId}, slug=${innerData?.slug ?? ""}, error=${response.error}, detail=${JSON.stringify(methodResult.error?.data ?? {})}`,
@@ -698,6 +721,51 @@ async function handleCustomMessageType(
698
721
  response.error = `Unknown message type: ${msgType}`;
699
722
  }
700
723
 
724
+ publishCustomMessageResponse(response, innerData, mqttTopic);
725
+ }
726
+
727
+ async function runCustomMessageHandler(
728
+ msgType: string,
729
+ handlerMethod: Function,
730
+ innerData: any,
731
+ baseResponse: any,
732
+ mqttTopic: string,
733
+ ): Promise<void> {
734
+ const response = {
735
+ ...baseResponse,
736
+ timestamp: Date.now(),
737
+ };
738
+
739
+ try {
740
+ const methodResult = await handlerMethod.call(messageHandler, innerData);
741
+
742
+ if (
743
+ typeof methodResult === "object" &&
744
+ methodResult !== null &&
745
+ "ok" in methodResult
746
+ ) {
747
+ response.success = methodResult.ok;
748
+ response.data = methodResult.result;
749
+ if (!methodResult.ok) {
750
+ response.error = methodResult.error?.message || "Unknown error";
751
+ } else {
752
+ delete response.error;
753
+ }
754
+ } else {
755
+ response.success = true;
756
+ response.data = methodResult;
757
+ delete response.error;
758
+ }
759
+ } catch (handlerErr: any) {
760
+ response.success = false;
761
+ response.data = undefined;
762
+ response.error = handlerErr.message;
763
+ }
764
+
765
+ publishCustomMessageResponse(response, innerData, mqttTopic);
766
+ }
767
+
768
+ function publishCustomMessageResponse(response: any, innerData: any, mqttTopic: string): void {
701
769
  const conn = ConnectionManager.getGlobalConnection();
702
770
  if (conn && conn.ws && conn.ws.connected) {
703
771
  // 根据 source_type 修改 topic 末尾的 #
@@ -10,10 +10,12 @@ import { createAgent, deleteAgent, listAgents, updateAgent } from './src/admin/m
10
10
  import {
11
11
  createArtifactRecord,
12
12
  ensureArtifactUploaded,
13
+ findLatestArtifacts,
13
14
  getArtifactContent,
14
15
  getArtifactPresignedPost,
15
16
  listArtifacts,
16
17
  markArtifactUploaded,
18
+ publishArtifact,
17
19
  refreshArtifacts
18
20
  } from './src/admin/methods/artifacts.js';
19
21
  import {
@@ -493,6 +495,13 @@ export class MessageHandler {
493
495
  });
494
496
  }
495
497
 
498
+ async artifactsFindLatest(data: any): Promise<any> {
499
+ return wrapAdminCall(async () => {
500
+ const context = getContext();
501
+ return await findLatestArtifacts(data, context);
502
+ });
503
+ }
504
+
496
505
  async artifactsGetContent(data: any): Promise<any> {
497
506
  return wrapAdminCall(async () => {
498
507
  const context = getContext();
@@ -507,6 +516,13 @@ export class MessageHandler {
507
516
  });
508
517
  }
509
518
 
519
+ async artifactsPublish(data: any): Promise<any> {
520
+ return wrapAdminCall(async () => {
521
+ const context = getContext();
522
+ return await publishArtifact(data, context);
523
+ });
524
+ }
525
+
510
526
  async artifactsGetPresignedPost(data: any): Promise<any> {
511
527
  return wrapAdminCall(async () => {
512
528
  const context = getContext();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rol-websocket-channel",
3
- "version": "1.6.2",
3
+ "version": "1.6.4",
4
4
  "description": "Unified OpenClaw plugin: MQTT Channel + Admin Bridge for remote management",
5
5
  "license": "MIT",
6
6
  "author": "nixgnehc",
@@ -67,28 +67,16 @@ test('setApiCoreBotConfig writes apiCoreBot endpoint without changing pairing or
67
67
  );
68
68
  });
69
69
 
70
- test('setApiCoreBotConfig uses default apiCoreBot values when omitted', async () => {
70
+ test('setApiCoreBotConfig rejects missing baseUrl instead of writing defaults', async () => {
71
71
  const context = await createMethodContext();
72
- await fs.writeFile(path.join(context.openclawRoot, 'openclaw.json'), '{}');
73
-
74
- const result = await setApiCoreBotConfig({}, context) as {
75
- apiCoreBot: {
76
- baseUrl: string;
77
- authToken: string;
78
- };
79
- };
80
- const savedConfig = JSON.parse(await fs.readFile(path.join(context.openclawRoot, 'openclaw.json'), 'utf8'));
72
+ const configPath = path.join(context.openclawRoot, 'openclaw.json');
73
+ await fs.writeFile(configPath, '{}');
81
74
 
82
- assert.equal(result.apiCoreBot.baseUrl, 'http://192.168.1.23:9092');
83
- assert.equal(result.apiCoreBot.authToken, '********');
84
- assert.equal(
85
- savedConfig.plugins.entries['rol-websocket-channel'].config.apiCoreBot.baseUrl,
86
- 'http://192.168.1.23:9092'
87
- );
88
- assert.equal(
89
- savedConfig.plugins.entries['rol-websocket-channel'].config.apiCoreBot.authToken,
90
- '123'
75
+ await assert.rejects(
76
+ () => setApiCoreBotConfig({}, context),
77
+ /apiCoreBot\.baseUrl is required/
91
78
  );
79
+ assert.equal(await fs.readFile(configPath, 'utf8'), '{}');
92
80
  });
93
81
 
94
82
  async function createMethodContext(): Promise<MethodContext> {
@@ -5,8 +5,6 @@ import { JsonRpcException, JSON_RPC_ERRORS } from '../jsonrpc.js';
5
5
  import type { JsonValue, MethodHandler } from '../types.js';
6
6
 
7
7
  const DEFAULT_PLUGIN_ID = 'rol-websocket-channel';
8
- const DEFAULT_API_CORE_BOT_BASE_URL = 'http://192.168.1.23:9092';
9
- const DEFAULT_API_CORE_BOT_AUTH_TOKEN = '123';
10
8
 
11
9
  interface OpenClawConfig {
12
10
  agents?: {
@@ -63,10 +61,10 @@ export const setApiCoreBotConfig: MethodHandler = async (params, context): Promi
63
61
  const objectParams = isObject(params) ? params : {};
64
62
  const baseUrl = typeof objectParams.baseUrl === 'string'
65
63
  ? objectParams.baseUrl.trim().replace(/\/+$/, '')
66
- : DEFAULT_API_CORE_BOT_BASE_URL;
64
+ : '';
67
65
  const authToken = typeof objectParams.authToken === 'string' && objectParams.authToken.trim()
68
66
  ? objectParams.authToken.trim()
69
- : DEFAULT_API_CORE_BOT_AUTH_TOKEN;
67
+ : null;
70
68
 
71
69
  if (!baseUrl) {
72
70
  throw new JsonRpcException(
@@ -53,7 +53,7 @@ function resolveOpenClawBin(): string {
53
53
  export async function installMem9(context: MethodContext): Promise<JsonValue> {
54
54
  const config = await ensureOpenClawConfigExists(context.openclawRoot);
55
55
  const currentState = readMem9State(config);
56
- const currentEntrypoint = await findMem9RuntimeEntrypoint(context.openclawRoot);
56
+ const currentEntrypoint = await findMem9RuntimeEntrypoint(context.openclawRoot, config);
57
57
 
58
58
  // Phase A: Plugin not installed → install only, then restart
59
59
  if (!currentState.installed && !currentEntrypoint) {
@@ -74,7 +74,7 @@ export async function installMem9(context: MethodContext): Promise<JsonValue> {
74
74
 
75
75
  // Phase B: Installed but no key → create key, write config, restart
76
76
  if (!currentState.configured || !currentState.apiKey) {
77
- const runtimeEntrypoint = await ensureMem9RuntimeEntrypoint(context.openclawRoot);
77
+ const runtimeEntrypoint = await ensureMem9RuntimeEntrypoint(context.openclawRoot, config);
78
78
  const apiKey = await createMem9Key();
79
79
  const updated = await writeMem9Config(context.openclawRoot, apiKey);
80
80
  const restart = await restartGateway(context.projectRoot);
@@ -97,7 +97,7 @@ export async function installMem9(context: MethodContext): Promise<JsonValue> {
97
97
  }
98
98
 
99
99
  // Phase C: Already configured → ensure slot/hooks/allow are correct
100
- const runtimeEntrypoint = await ensureMem9RuntimeEntrypoint(context.openclawRoot);
100
+ const runtimeEntrypoint = await ensureMem9RuntimeEntrypoint(context.openclawRoot, config);
101
101
  const updated = await ensureMem9SlotConfig(context.openclawRoot, currentState.apiKey!);
102
102
  const restart = await restartGateway(context.projectRoot);
103
103
 
@@ -130,7 +130,7 @@ export async function reconnectMem9(key: string, context: MethodContext): Promis
130
130
 
131
131
  const config = await ensureOpenClawConfigExists(context.openclawRoot);
132
132
  const previousState = readMem9State(config);
133
- const runtimeEntrypoint = await ensureMem9RuntimeEntrypoint(context.openclawRoot);
133
+ const runtimeEntrypoint = await ensureMem9RuntimeEntrypoint(context.openclawRoot, config);
134
134
  const updated = await writeMem9Config(context.openclawRoot, apiKey);
135
135
  const restart = await restartGateway(context.projectRoot);
136
136
 
@@ -225,8 +225,8 @@ async function installMem9Plugin(cwd: string): Promise<void> {
225
225
  }
226
226
  }
227
227
 
228
- export async function findMem9RuntimeEntrypoint(openclawRoot: string): Promise<string | null> {
229
- for (const packageRoot of MEM9_PACKAGE_ROOTS.map((item) => path.join(openclawRoot, item))) {
228
+ export async function findMem9RuntimeEntrypoint(openclawRoot: string, config?: OpenClawConfig): Promise<string | null> {
229
+ for (const packageRoot of resolveMem9RuntimePackageRoots(openclawRoot, config)) {
230
230
  for (const entrypoint of RUNTIME_ENTRYPOINTS.map((item) => path.join(packageRoot, item))) {
231
231
  if (await pathExists(entrypoint)) {
232
232
  return entrypoint;
@@ -236,22 +236,40 @@ export async function findMem9RuntimeEntrypoint(openclawRoot: string): Promise<s
236
236
  return null;
237
237
  }
238
238
 
239
- async function ensureMem9RuntimeEntrypoint(openclawRoot: string): Promise<string> {
240
- const entrypoint = await findMem9RuntimeEntrypoint(openclawRoot);
239
+ async function ensureMem9RuntimeEntrypoint(openclawRoot: string, config?: OpenClawConfig): Promise<string> {
240
+ const entrypoint = await findMem9RuntimeEntrypoint(openclawRoot, config);
241
241
  if (entrypoint) {
242
242
  return entrypoint;
243
243
  }
244
+ const installRecord = readMem9InstallRecord(config);
245
+ const checkedPackageRoots = resolveMem9RuntimePackageRoots(openclawRoot, config);
244
246
  throw new JsonRpcException(
245
247
  JSON_RPC_ERRORS.internalError,
246
248
  'mem9 plugin is installed but missing compiled runtime output',
247
249
  {
248
250
  code: 'MEM9_RUNTIME_OUTPUT_MISSING',
249
251
  expected: RUNTIME_ENTRYPOINTS.map((item) => `./${item.replace(/\\/g, '/')}`),
250
- packageRoots: MEM9_PACKAGE_ROOTS.map((item) => path.join(openclawRoot, item))
252
+ checkedPackageRoots,
253
+ checkedEntrypoints: checkedPackageRoots.flatMap((packageRoot) => RUNTIME_ENTRYPOINTS.map((item) => path.join(packageRoot, item))),
254
+ installRecord
251
255
  }
252
256
  );
253
257
  }
254
258
 
259
+ function resolveMem9RuntimePackageRoots(openclawRoot: string, config?: OpenClawConfig): string[] {
260
+ const roots: string[] = [];
261
+ const installPath = pickString(readMem9InstallRecord(config)?.installPath);
262
+ if (installPath) {
263
+ roots.push(path.isAbsolute(installPath) ? installPath : path.resolve(openclawRoot, installPath));
264
+ }
265
+ for (const fallbackRoot of MEM9_PACKAGE_ROOTS.map((item) => path.join(openclawRoot, item))) {
266
+ if (!roots.includes(fallbackRoot)) {
267
+ roots.push(fallbackRoot);
268
+ }
269
+ }
270
+ return roots;
271
+ }
272
+
255
273
  async function createMem9Key(): Promise<string> {
256
274
  let response: Response;
257
275
  try {
@@ -447,6 +465,11 @@ function isRecord(value: unknown): value is Record<string, any> {
447
465
  return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
448
466
  }
449
467
 
468
+ function readMem9InstallRecord(config?: OpenClawConfig): Record<string, any> | null {
469
+ const record = config?.plugins?.installs?.[MEM9_PLUGIN_ID];
470
+ return isRecord(record) ? record : null;
471
+ }
472
+
450
473
  function readMem9State(config: OpenClawConfig): {
451
474
  installed: boolean;
452
475
  configured: boolean;
@@ -62,18 +62,33 @@ export const updateModels: MethodHandler = async (
62
62
 
63
63
  // 更新 primary model
64
64
  if (primaryModel) {
65
- if (!config.agents) config.agents = {};
66
- if (!config.agents.defaults) config.agents.defaults = {};
67
- if (!config.agents.defaults.model) config.agents.defaults.model = {};
68
-
69
- config.agents.defaults.model.primary = primaryModel;
70
65
  const inferredProvider = inferProviderFromPrimaryModel(primaryModel);
66
+ if (!inferredProvider) {
67
+ throwModelError(
68
+ 'MODEL_PRIMARY_INVALID',
69
+ 'primaryModel must be in provider/model format'
70
+ );
71
+ }
72
+
71
73
  if (modelProvider && modelProvider !== inferredProvider) {
72
74
  throwModelError(
73
75
  'MODEL_PROVIDER_MISMATCH',
74
76
  'modelProvider does not match primaryModel'
75
77
  );
76
78
  }
79
+
80
+ if (!isAllowedModel(config, primaryModel)) {
81
+ throwModelError(
82
+ 'MODEL_NOT_ALLOWED',
83
+ 'model is not in allowed model options'
84
+ );
85
+ }
86
+
87
+ if (!config.agents) config.agents = {};
88
+ if (!config.agents.defaults) config.agents.defaults = {};
89
+ if (!config.agents.defaults.model) config.agents.defaults.model = {};
90
+
91
+ config.agents.defaults.model.primary = primaryModel;
77
92
  updated = true;
78
93
  changes.push(`Updated primary model to: ${primaryModel}`);
79
94
  }