rol-websocket-channel 1.6.0 → 1.6.3

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
  // 兼容两种返回格式:
@@ -545,6 +559,41 @@ async function handleCustomMessageType(msgType, innerData, traceId, accountId, m
545
559
  response.success = false;
546
560
  response.error = `Unknown message type: ${msgType}`;
547
561
  }
562
+ publishCustomMessageResponse(response, innerData, mqttTopic);
563
+ }
564
+ async function runCustomMessageHandler(msgType, handlerMethod, innerData, baseResponse, mqttTopic) {
565
+ const response = {
566
+ ...baseResponse,
567
+ timestamp: Date.now(),
568
+ };
569
+ try {
570
+ const methodResult = await handlerMethod.call(messageHandler, innerData);
571
+ if (typeof methodResult === "object" &&
572
+ methodResult !== null &&
573
+ "ok" in methodResult) {
574
+ response.success = methodResult.ok;
575
+ response.data = methodResult.result;
576
+ if (!methodResult.ok) {
577
+ response.error = methodResult.error?.message || "Unknown error";
578
+ }
579
+ else {
580
+ delete response.error;
581
+ }
582
+ }
583
+ else {
584
+ response.success = true;
585
+ response.data = methodResult;
586
+ delete response.error;
587
+ }
588
+ }
589
+ catch (handlerErr) {
590
+ response.success = false;
591
+ response.data = undefined;
592
+ response.error = handlerErr.message;
593
+ }
594
+ publishCustomMessageResponse(response, innerData, mqttTopic);
595
+ }
596
+ function publishCustomMessageResponse(response, innerData, mqttTopic) {
548
597
  const conn = ConnectionManager.getGlobalConnection();
549
598
  if (conn && conn.ws && conn.ws.connected) {
550
599
  // 根据 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-'));
@@ -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
  }
@@ -8,7 +8,7 @@ const execAsync = promisify(exec);
8
8
  const execFileAsync = promisify(execFile);
9
9
  const UPDATE_COMMAND_TIMEOUT_MS = 10 * 60 * 1000;
10
10
  const UPDATE_COMMAND_MAX_BUFFER = 10 * 1024 * 1024;
11
- const OPENCLAW_UPDATE_TARGET_PACKAGE = '@openclaw/core';
11
+ const OPENCLAW_UPDATE_TARGET_PACKAGE = 'openclaw';
12
12
  const OPENCLAW_UPDATE_TARGET_VERSION = '2026.5.6';
13
13
  const CHANNEL_FALLBACK_VERSION = '1.5.9';
14
14
  export const ping = async () => {
@@ -76,8 +76,7 @@ export const doctorFix = async (_params, context) => {
76
76
  }
77
77
  };
78
78
  export const openclawUpdate = async (_params, context) => {
79
- const packageSpec = `${OPENCLAW_UPDATE_TARGET_PACKAGE}@${OPENCLAW_UPDATE_TARGET_VERSION}`;
80
- const installResult = await runSystemCommand(process.env.NPM_BIN || 'npm', ['install', '-g', packageSpec], context.openclawRoot, context, 'openclawUpdate.installCore');
79
+ const updateResult = await runOpenClawCommand(['update', '--tag', OPENCLAW_UPDATE_TARGET_VERSION], context, 'openclawUpdate.update');
81
80
  const versionResult = await runOpenClawCommand(['--version'], context, 'openclawUpdate.version');
82
81
  const doctorResult = await runOpenClawCommand(['doctor', '--deep'], context, 'openclawUpdate.doctorDeep');
83
82
  return {
@@ -87,7 +86,7 @@ export const openclawUpdate = async (_params, context) => {
87
86
  targetVersion: OPENCLAW_UPDATE_TARGET_VERSION,
88
87
  restartRecommended: true,
89
88
  steps: [
90
- buildCommandStep('installCore', installResult),
89
+ buildCommandStep('update', updateResult),
91
90
  buildCommandStep('version', versionResult),
92
91
  buildCommandStep('doctorDeep', doctorResult)
93
92
  ]
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
 
@@ -698,6 +715,51 @@ async function handleCustomMessageType(
698
715
  response.error = `Unknown message type: ${msgType}`;
699
716
  }
700
717
 
718
+ publishCustomMessageResponse(response, innerData, mqttTopic);
719
+ }
720
+
721
+ async function runCustomMessageHandler(
722
+ msgType: string,
723
+ handlerMethod: Function,
724
+ innerData: any,
725
+ baseResponse: any,
726
+ mqttTopic: string,
727
+ ): Promise<void> {
728
+ const response = {
729
+ ...baseResponse,
730
+ timestamp: Date.now(),
731
+ };
732
+
733
+ try {
734
+ const methodResult = await handlerMethod.call(messageHandler, innerData);
735
+
736
+ if (
737
+ typeof methodResult === "object" &&
738
+ methodResult !== null &&
739
+ "ok" in methodResult
740
+ ) {
741
+ response.success = methodResult.ok;
742
+ response.data = methodResult.result;
743
+ if (!methodResult.ok) {
744
+ response.error = methodResult.error?.message || "Unknown error";
745
+ } else {
746
+ delete response.error;
747
+ }
748
+ } else {
749
+ response.success = true;
750
+ response.data = methodResult;
751
+ delete response.error;
752
+ }
753
+ } catch (handlerErr: any) {
754
+ response.success = false;
755
+ response.data = undefined;
756
+ response.error = handlerErr.message;
757
+ }
758
+
759
+ publishCustomMessageResponse(response, innerData, mqttTopic);
760
+ }
761
+
762
+ function publishCustomMessageResponse(response: any, innerData: any, mqttTopic: string): void {
701
763
  const conn = ConnectionManager.getGlobalConnection();
702
764
  if (conn && conn.ws && conn.ws.connected) {
703
765
  // 根据 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.0",
3
+ "version": "1.6.3",
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(
@@ -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
  }
@@ -10,7 +10,7 @@ const execAsync = promisify(exec);
10
10
  const execFileAsync = promisify(execFile);
11
11
  const UPDATE_COMMAND_TIMEOUT_MS = 10 * 60 * 1000;
12
12
  const UPDATE_COMMAND_MAX_BUFFER = 10 * 1024 * 1024;
13
- const OPENCLAW_UPDATE_TARGET_PACKAGE = '@openclaw/core';
13
+ const OPENCLAW_UPDATE_TARGET_PACKAGE = 'openclaw';
14
14
  const OPENCLAW_UPDATE_TARGET_VERSION = '2026.5.6';
15
15
  const CHANNEL_FALLBACK_VERSION = '1.5.9';
16
16
 
@@ -83,13 +83,10 @@ export const doctorFix: MethodHandler = async (_params, context: MethodContext):
83
83
  };
84
84
 
85
85
  export const openclawUpdate: MethodHandler = async (_params, context: MethodContext): Promise<JsonValue> => {
86
- const packageSpec = `${OPENCLAW_UPDATE_TARGET_PACKAGE}@${OPENCLAW_UPDATE_TARGET_VERSION}`;
87
- const installResult = await runSystemCommand(
88
- process.env.NPM_BIN || 'npm',
89
- ['install', '-g', packageSpec],
90
- context.openclawRoot,
86
+ const updateResult = await runOpenClawCommand(
87
+ ['update', '--tag', OPENCLAW_UPDATE_TARGET_VERSION],
91
88
  context,
92
- 'openclawUpdate.installCore'
89
+ 'openclawUpdate.update'
93
90
  );
94
91
  const versionResult = await runOpenClawCommand(['--version'], context, 'openclawUpdate.version');
95
92
  const doctorResult = await runOpenClawCommand(['doctor', '--deep'], context, 'openclawUpdate.doctorDeep');
@@ -101,7 +98,7 @@ export const openclawUpdate: MethodHandler = async (_params, context: MethodCont
101
98
  targetVersion: OPENCLAW_UPDATE_TARGET_VERSION,
102
99
  restartRecommended: true,
103
100
  steps: [
104
- buildCommandStep('installCore', installResult),
101
+ buildCommandStep('update', updateResult),
105
102
  buildCommandStep('version', versionResult),
106
103
  buildCommandStep('doctorDeep', doctorResult)
107
104
  ]
@@ -0,0 +1,49 @@
1
+ import assert from 'node:assert/strict';
2
+ import fs from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import test from 'node:test';
6
+
7
+ import { updateModels } from '../../../src/admin/methods/models-extended.js';
8
+ import { JsonRpcException, JSON_RPC_ERRORS } from '../../../src/admin/jsonrpc.js';
9
+ import type { MethodContext } from '../../../src/admin/types.js';
10
+
11
+ test('updateModels rejects primaryModel values outside the allowed catalog', async () => {
12
+ const context = await createMethodContext();
13
+ await fs.writeFile(path.join(context.openclawRoot, 'openclaw.json'), JSON.stringify({
14
+ agents: {
15
+ defaults: {
16
+ model: {
17
+ primary: 'openai/gpt-5.4-mini'
18
+ },
19
+ models: {
20
+ 'openai/gpt-5.4-mini': {
21
+ label: 'GPT-5.4 Mini'
22
+ }
23
+ }
24
+ }
25
+ }
26
+ }, null, 2));
27
+
28
+ await assert.rejects(
29
+ updateModels({ primaryModel: 'openai/not-allowed' }, context),
30
+ (error: unknown) => {
31
+ assert.ok(error instanceof JsonRpcException);
32
+ assert.equal(error.code, JSON_RPC_ERRORS.invalidParams);
33
+ assert.equal(error.message, 'model is not in allowed model options');
34
+ assert.deepEqual(error.data, { code: 'MODEL_NOT_ALLOWED' });
35
+ return true;
36
+ }
37
+ );
38
+ });
39
+
40
+ async function createMethodContext(): Promise<MethodContext> {
41
+ const projectRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'models-extended-test-'));
42
+ const openclawRoot = path.join(projectRoot, '.openclaw');
43
+ await fs.mkdir(openclawRoot, { recursive: true });
44
+
45
+ return {
46
+ projectRoot,
47
+ openclawRoot
48
+ };
49
+ }
@@ -0,0 +1,77 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+
4
+ import { handleCustomMessageType } from '../index.js';
5
+ import { messageHandler } from '../message-handler.js';
6
+ import {
7
+ _setMqttConnectFn,
8
+ closeGlobalConnection,
9
+ createGlobalMqttConnection
10
+ } from '../src/mqtt/connection-manager.js';
11
+
12
+ test('openclawUpdate publishes an immediate running response before the command finishes', async () => {
13
+ const published: Array<{ topic: string; message: string }> = [];
14
+ const originalOpenclawUpdate = (messageHandler as any).openclawUpdate;
15
+ let finish!: () => void;
16
+ const commandFinished = new Promise<void>((resolve) => {
17
+ finish = resolve;
18
+ });
19
+
20
+ const fakeClient = {
21
+ connected: true,
22
+ on() {},
23
+ subscribe() {},
24
+ end() {},
25
+ publish(topic: string, message: string) {
26
+ published.push({ topic, message });
27
+ }
28
+ };
29
+
30
+ _setMqttConnectFn(() => fakeClient as any);
31
+ await createGlobalMqttConnection('mqtt://test', 'announcement/user/agent/#', {});
32
+
33
+ try {
34
+ (messageHandler as any).openclawUpdate = async () => {
35
+ await commandFinished;
36
+ return { ok: true, result: { ok: true, action: 'openclawUpdate' } };
37
+ };
38
+
39
+ const task = handleCustomMessageType(
40
+ 'openclawUpdate',
41
+ { source_type: 'device' },
42
+ 'trace-update-001',
43
+ 'default',
44
+ 'announcement/user/agent/#'
45
+ );
46
+
47
+ await new Promise((resolve) => setImmediate(resolve));
48
+
49
+ assert.equal(published.length, 1);
50
+ const first = JSON.parse(published[0]!.message);
51
+ assert.equal(published[0]!.topic, 'announcement/user/agent/device');
52
+ assert.equal(first.trace_id, 'trace-update-001');
53
+ assert.equal(first.success, true);
54
+ assert.equal(first.data.status, 'running');
55
+
56
+ await task;
57
+ finish();
58
+ await waitFor(() => published.length === 2);
59
+
60
+ assert.equal(published.length, 2);
61
+ const final = JSON.parse(published[1]!.message);
62
+ assert.equal(final.trace_id, 'trace-update-001');
63
+ assert.equal(final.success, true);
64
+ assert.deepEqual(final.data, { ok: true, action: 'openclawUpdate' });
65
+ } finally {
66
+ (messageHandler as any).openclawUpdate = originalOpenclawUpdate;
67
+ closeGlobalConnection();
68
+ _setMqttConnectFn(null);
69
+ }
70
+ });
71
+
72
+ async function waitFor(predicate: () => boolean): Promise<void> {
73
+ for (let i = 0; i < 20; i += 1) {
74
+ if (predicate()) return;
75
+ await new Promise((resolve) => setTimeout(resolve, 10));
76
+ }
77
+ }
@@ -0,0 +1,11 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+
4
+ import { MessageHandler } from '../message-handler.js';
5
+
6
+ test('message handler exposes artifact convenience MQTT methods', () => {
7
+ const handler = new MessageHandler() as any;
8
+
9
+ assert.equal(typeof handler.artifactsFindLatest, 'function');
10
+ assert.equal(typeof handler.artifactsPublish, 'function');
11
+ });