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 +50 -1
- package/dist/message-handler.js +13 -1
- package/dist/src/admin/methods/admin.js +2 -4
- package/dist/src/admin/methods/admin.test.js +5 -8
- package/dist/src/admin/methods/models-extended.js +10 -4
- package/dist/src/admin/methods/system.js +3 -4
- package/index.ts +63 -1
- package/message-handler.ts +16 -0
- package/package.json +1 -1
- package/src/admin/methods/admin.test.ts +7 -19
- package/src/admin/methods/admin.ts +2 -4
- package/src/admin/methods/models-extended.ts +20 -5
- package/src/admin/methods/system.ts +5 -8
- package/test/admin/methods/models-extended.test.ts +49 -0
- package/test/custom-message-update-ack.test.ts +77 -0
- package/test/message-handler-artifacts.test.ts +11 -0
- package/MQTT-API System.md +0 -2195
- package/MQTT-API/347/211/210/346/234/254/346/237/245/347/234/213.md +0 -2191
- package/SDK-CHANNEL-TODO.md +0 -6
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
|
-
|
|
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 末尾的 #
|
package/dist/message-handler.js
CHANGED
|
@@ -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
|
-
:
|
|
37
|
+
: '';
|
|
40
38
|
const authToken = typeof objectParams.authToken === 'string' && objectParams.authToken.trim()
|
|
41
39
|
? objectParams.authToken.trim()
|
|
42
|
-
:
|
|
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
|
|
46
|
+
test('setApiCoreBotConfig rejects missing baseUrl instead of writing defaults', async () => {
|
|
47
47
|
const context = await createMethodContext();
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
assert.equal(
|
|
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 = '
|
|
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
|
|
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('
|
|
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
|
-
|
|
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 末尾的 #
|
package/message-handler.ts
CHANGED
|
@@ -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
|
@@ -67,28 +67,16 @@ test('setApiCoreBotConfig writes apiCoreBot endpoint without changing pairing or
|
|
|
67
67
|
);
|
|
68
68
|
});
|
|
69
69
|
|
|
70
|
-
test('setApiCoreBotConfig
|
|
70
|
+
test('setApiCoreBotConfig rejects missing baseUrl instead of writing defaults', async () => {
|
|
71
71
|
const context = await createMethodContext();
|
|
72
|
-
|
|
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.
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
:
|
|
64
|
+
: '';
|
|
67
65
|
const authToken = typeof objectParams.authToken === 'string' && objectParams.authToken.trim()
|
|
68
66
|
? objectParams.authToken.trim()
|
|
69
|
-
:
|
|
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 = '
|
|
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
|
|
87
|
-
|
|
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.
|
|
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('
|
|
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
|
+
});
|