rol-websocket-channel 1.0.0 → 1.0.1
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/bin/rol.js +35 -0
- package/index.ts +63 -1
- package/message-handler.ts +8 -1
- package/openclaw.plugin.json +125 -0
- package/package.json +4 -1
- package/readme.md +2 -1
- package/src/admin/methods/index.ts +2 -1
- package/src/admin/methods/models-extended.ts +85 -0
- package/src/admin/methods/models.ts +186 -6
- package/src/admin/methods/pairing.ts +314 -0
- package/MQTT-API.md +0 -967
package/bin/rol.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
|
|
4
|
+
const args = process.argv.slice(2);
|
|
5
|
+
|
|
6
|
+
if (args.length === 0) {
|
|
7
|
+
printUsageAndExit(1);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const [command, ...rest] = args;
|
|
11
|
+
|
|
12
|
+
if (command !== 'pair') {
|
|
13
|
+
printUsageAndExit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (rest.length === 0 || rest[0].startsWith('-')) {
|
|
17
|
+
printUsageAndExit(1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const result = spawnSync('openclaw', ['admin-bridge', 'pair', ...rest], {
|
|
21
|
+
stdio: 'inherit',
|
|
22
|
+
shell: process.platform === 'win32'
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
if (result.error) {
|
|
26
|
+
process.stderr.write(`${result.error.message}\n`);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
process.exit(result.status ?? 0);
|
|
31
|
+
|
|
32
|
+
function printUsageAndExit(code) {
|
|
33
|
+
process.stderr.write('Usage: rol pair <key> [--endpoint <url>] [--auth <token>]\n');
|
|
34
|
+
process.exit(code);
|
|
35
|
+
}
|
package/index.ts
CHANGED
|
@@ -30,7 +30,7 @@ interface WebSocketChannelAccount {
|
|
|
30
30
|
// ============================================
|
|
31
31
|
// 3. 全局状态
|
|
32
32
|
// ============================================
|
|
33
|
-
import { initializeContext } from "./src/shared/context.js";
|
|
33
|
+
import { getContext, initializeContext } from "./src/shared/context.js";
|
|
34
34
|
|
|
35
35
|
let pluginRuntime: any = null;
|
|
36
36
|
|
|
@@ -72,6 +72,33 @@ const WebSocketChannel: ChannelPlugin<WebSocketChannelAccount> = {
|
|
|
72
72
|
additionalProperties: false,
|
|
73
73
|
properties: {
|
|
74
74
|
enabled: { type: "boolean" },
|
|
75
|
+
dmPolicy: {
|
|
76
|
+
type: "string",
|
|
77
|
+
enum: ["pairing", "allowlist", "open", "disabled"],
|
|
78
|
+
},
|
|
79
|
+
allowFrom: {
|
|
80
|
+
type: "array",
|
|
81
|
+
items: { type: "string" }
|
|
82
|
+
},
|
|
83
|
+
pairing: {
|
|
84
|
+
type: "object",
|
|
85
|
+
additionalProperties: false,
|
|
86
|
+
properties: {
|
|
87
|
+
paired: { type: "boolean" },
|
|
88
|
+
pairedAt: { type: "string" },
|
|
89
|
+
pairingKeyLast4: { type: "string" },
|
|
90
|
+
userId: { type: "string" },
|
|
91
|
+
rawValue: { type: "string" }
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
apiCoreBot: {
|
|
95
|
+
type: "object",
|
|
96
|
+
additionalProperties: false,
|
|
97
|
+
properties: {
|
|
98
|
+
baseUrl: { type: "string" },
|
|
99
|
+
authToken: { type: "string" }
|
|
100
|
+
}
|
|
101
|
+
},
|
|
75
102
|
config: {
|
|
76
103
|
type: "object",
|
|
77
104
|
additionalProperties: false,
|
|
@@ -90,6 +117,10 @@ const WebSocketChannel: ChannelPlugin<WebSocketChannelAccount> = {
|
|
|
90
117
|
},
|
|
91
118
|
uiHints: {
|
|
92
119
|
enabled: { label: "Enabled", description: "Enable MQTT Channel" },
|
|
120
|
+
dmPolicy: { label: "DM Policy", description: "Pairing/allowlist/open policy for direct messages" },
|
|
121
|
+
allowFrom: { label: "Allow From", description: "Allowed sender IDs when using allowlist or pairing mode" },
|
|
122
|
+
pairing: { label: "Pairing", description: "Pairing status and resolved identity info" },
|
|
123
|
+
apiCoreBot: { label: "API Core Bot", description: "Backend API endpoint and auth token used by the plugin" },
|
|
93
124
|
config: { label: "Configuration", description: "MQTT connection configuration" },
|
|
94
125
|
"config.enabled": { label: "Enabled", description: "Enable this configuration" },
|
|
95
126
|
"config.mqttUrl": {
|
|
@@ -128,6 +159,8 @@ const WebSocketChannel: ChannelPlugin<WebSocketChannelAccount> = {
|
|
|
128
159
|
mqttUrl: config.mqttUrl || "ws://192.168.1.23:8083/mqtt",
|
|
129
160
|
mqttTopic: config.mqttTopic || "announcement/tester",
|
|
130
161
|
enabled: config.enabled !== false,
|
|
162
|
+
dmPolicy: channelCfg.dmPolicy || config.groupPolicy || "open",
|
|
163
|
+
allowFrom: Array.isArray(channelCfg.allowFrom) ? channelCfg.allowFrom : [],
|
|
131
164
|
groupPolicy: config.groupPolicy || "open",
|
|
132
165
|
};
|
|
133
166
|
},
|
|
@@ -504,6 +537,35 @@ function registerAdminBridgeCli(api: any) {
|
|
|
504
537
|
}, null, 2) + '\n');
|
|
505
538
|
}
|
|
506
539
|
});
|
|
540
|
+
|
|
541
|
+
root
|
|
542
|
+
.command('pair <key>')
|
|
543
|
+
.description('Pair rol-websocket-channel and write OpenClaw configuration')
|
|
544
|
+
.option('--endpoint <url>', 'Pair exchange endpoint')
|
|
545
|
+
.option('--auth <token>', 'Authorization header value for pair exchange')
|
|
546
|
+
.action(async (key: string, options: { endpoint?: string; auth?: string }) => {
|
|
547
|
+
try {
|
|
548
|
+
const { pairWithKey } = await import('./src/admin/methods/pairing.js');
|
|
549
|
+
const result = await pairWithKey(
|
|
550
|
+
{
|
|
551
|
+
key,
|
|
552
|
+
endpoint: options.endpoint,
|
|
553
|
+
auth: options.auth
|
|
554
|
+
},
|
|
555
|
+
getContext()
|
|
556
|
+
);
|
|
557
|
+
process.stdout.write(JSON.stringify({ ok: true, result }, null, 2) + '\n');
|
|
558
|
+
} catch (error) {
|
|
559
|
+
process.exitCode = 1;
|
|
560
|
+
process.stderr.write(JSON.stringify({
|
|
561
|
+
ok: false,
|
|
562
|
+
error: {
|
|
563
|
+
message: error instanceof Error ? error.message : String(error),
|
|
564
|
+
code: (error as any)?.data?.code
|
|
565
|
+
}
|
|
566
|
+
}, null, 2) + '\n');
|
|
567
|
+
}
|
|
568
|
+
});
|
|
507
569
|
}, {
|
|
508
570
|
descriptors: [{
|
|
509
571
|
name: 'admin-bridge',
|
package/message-handler.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { createAgent, deleteAgent, listAgents, updateAgent } from './src/admin/m
|
|
|
10
10
|
import { listSessions } from './src/admin/methods/sessions.js';
|
|
11
11
|
import { getSession, prepareMessage, attachSkill } from './src/admin/methods/sessions-extended.js';
|
|
12
12
|
import { getModels } from './src/admin/methods/models.js';
|
|
13
|
-
import { updateModels } from './src/admin/methods/models-extended.js';
|
|
13
|
+
import { setModel, updateModels } from './src/admin/methods/models-extended.js';
|
|
14
14
|
import {
|
|
15
15
|
getUsagePageSummary,
|
|
16
16
|
getUsageTimeseries,
|
|
@@ -163,6 +163,13 @@ export class MessageHandler {
|
|
|
163
163
|
});
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
+
async modelsSet(data: any): Promise<any> {
|
|
167
|
+
return wrapAdminCall(async () => {
|
|
168
|
+
const context = getContext();
|
|
169
|
+
return await setModel(data, context);
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
166
173
|
/**
|
|
167
174
|
* 获取 usage summary
|
|
168
175
|
*/
|
package/openclaw.plugin.json
CHANGED
|
@@ -9,6 +9,52 @@
|
|
|
9
9
|
"type": "object",
|
|
10
10
|
"additionalProperties": false,
|
|
11
11
|
"properties": {
|
|
12
|
+
"enabled": {
|
|
13
|
+
"type": "boolean"
|
|
14
|
+
},
|
|
15
|
+
"dmPolicy": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"enum": ["pairing", "allowlist", "open", "disabled"]
|
|
18
|
+
},
|
|
19
|
+
"allowFrom": {
|
|
20
|
+
"type": "array",
|
|
21
|
+
"items": {
|
|
22
|
+
"type": "string"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"pairing": {
|
|
26
|
+
"type": "object",
|
|
27
|
+
"additionalProperties": false,
|
|
28
|
+
"properties": {
|
|
29
|
+
"paired": {
|
|
30
|
+
"type": "boolean"
|
|
31
|
+
},
|
|
32
|
+
"pairedAt": {
|
|
33
|
+
"type": "string"
|
|
34
|
+
},
|
|
35
|
+
"pairingKeyLast4": {
|
|
36
|
+
"type": "string"
|
|
37
|
+
},
|
|
38
|
+
"userId": {
|
|
39
|
+
"type": "string"
|
|
40
|
+
},
|
|
41
|
+
"rawValue": {
|
|
42
|
+
"type": "string"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"apiCoreBot": {
|
|
47
|
+
"type": "object",
|
|
48
|
+
"additionalProperties": false,
|
|
49
|
+
"properties": {
|
|
50
|
+
"baseUrl": {
|
|
51
|
+
"type": "string"
|
|
52
|
+
},
|
|
53
|
+
"authToken": {
|
|
54
|
+
"type": "string"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
},
|
|
12
58
|
"channels": {
|
|
13
59
|
"type": "object",
|
|
14
60
|
"additionalProperties": false,
|
|
@@ -20,6 +66,49 @@
|
|
|
20
66
|
"enabled": {
|
|
21
67
|
"type": "boolean"
|
|
22
68
|
},
|
|
69
|
+
"pairing": {
|
|
70
|
+
"type": "object",
|
|
71
|
+
"additionalProperties": false,
|
|
72
|
+
"properties": {
|
|
73
|
+
"paired": {
|
|
74
|
+
"type": "boolean"
|
|
75
|
+
},
|
|
76
|
+
"pairedAt": {
|
|
77
|
+
"type": "string"
|
|
78
|
+
},
|
|
79
|
+
"pairingKeyLast4": {
|
|
80
|
+
"type": "string"
|
|
81
|
+
},
|
|
82
|
+
"userId": {
|
|
83
|
+
"type": "string"
|
|
84
|
+
},
|
|
85
|
+
"rawValue": {
|
|
86
|
+
"type": "string"
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
"apiCoreBot": {
|
|
91
|
+
"type": "object",
|
|
92
|
+
"additionalProperties": false,
|
|
93
|
+
"properties": {
|
|
94
|
+
"baseUrl": {
|
|
95
|
+
"type": "string"
|
|
96
|
+
},
|
|
97
|
+
"authToken": {
|
|
98
|
+
"type": "string"
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
"dmPolicy": {
|
|
103
|
+
"type": "string",
|
|
104
|
+
"enum": ["pairing", "allowlist", "open", "disabled"]
|
|
105
|
+
},
|
|
106
|
+
"allowFrom": {
|
|
107
|
+
"type": "array",
|
|
108
|
+
"items": {
|
|
109
|
+
"type": "string"
|
|
110
|
+
}
|
|
111
|
+
},
|
|
23
112
|
"config": {
|
|
24
113
|
"type": "object",
|
|
25
114
|
"additionalProperties": false,
|
|
@@ -48,6 +137,26 @@
|
|
|
48
137
|
},
|
|
49
138
|
|
|
50
139
|
"uiHints": {
|
|
140
|
+
"enabled": {
|
|
141
|
+
"label": "Enabled",
|
|
142
|
+
"description": "Enable plugin config"
|
|
143
|
+
},
|
|
144
|
+
"pairing": {
|
|
145
|
+
"label": "Pairing",
|
|
146
|
+
"description": "Pairing status and resolved identity info"
|
|
147
|
+
},
|
|
148
|
+
"apiCoreBot": {
|
|
149
|
+
"label": "API Core Bot",
|
|
150
|
+
"description": "Backend API endpoint and auth token used by the plugin"
|
|
151
|
+
},
|
|
152
|
+
"dmPolicy": {
|
|
153
|
+
"label": "DM Policy",
|
|
154
|
+
"description": "Pairing/allowlist/open policy for direct messages"
|
|
155
|
+
},
|
|
156
|
+
"allowFrom": {
|
|
157
|
+
"label": "Allow From",
|
|
158
|
+
"description": "Allowed sender IDs when using allowlist or pairing mode"
|
|
159
|
+
},
|
|
51
160
|
"channels": {
|
|
52
161
|
"label": "Channels"
|
|
53
162
|
},
|
|
@@ -58,6 +167,22 @@
|
|
|
58
167
|
"label": "Enabled",
|
|
59
168
|
"description": "Enable MQTT Channel"
|
|
60
169
|
},
|
|
170
|
+
"channels.rol-websocket-channel.pairing": {
|
|
171
|
+
"label": "Pairing",
|
|
172
|
+
"description": "Pairing status and resolved identity info"
|
|
173
|
+
},
|
|
174
|
+
"channels.rol-websocket-channel.apiCoreBot": {
|
|
175
|
+
"label": "API Core Bot",
|
|
176
|
+
"description": "Backend API endpoint and auth token used by the plugin"
|
|
177
|
+
},
|
|
178
|
+
"channels.rol-websocket-channel.dmPolicy": {
|
|
179
|
+
"label": "DM Policy",
|
|
180
|
+
"description": "Pairing/allowlist/open policy for direct messages"
|
|
181
|
+
},
|
|
182
|
+
"channels.rol-websocket-channel.allowFrom": {
|
|
183
|
+
"label": "Allow From",
|
|
184
|
+
"description": "Allowed sender IDs when using allowlist or pairing mode"
|
|
185
|
+
},
|
|
61
186
|
"channels.rol-websocket-channel.config": {
|
|
62
187
|
"label": "Configuration",
|
|
63
188
|
"description": "MQTT/WebSocket connection configuration"
|
package/package.json
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rol-websocket-channel",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Unified OpenClaw plugin: MQTT Channel + Admin Bridge for remote management",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "nixgnehc",
|
|
7
7
|
"type": "module",
|
|
8
|
+
"bin": {
|
|
9
|
+
"rol": "./bin/rol.js"
|
|
10
|
+
},
|
|
8
11
|
"keywords": [
|
|
9
12
|
"openclaw",
|
|
10
13
|
"mqtt",
|
package/readme.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
|
|
2
2
|
|
|
3
3
|
```
|
|
4
|
-
openclaw plugins install
|
|
4
|
+
openclaw plugins install rol-websocket-channel
|
|
5
|
+
openclaw plugins install "c:\Users\admin\Desktop\openclaw-demo\websocket-channel" --force
|
|
5
6
|
|
|
6
7
|
topic
|
|
7
8
|
announcement/tester
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
listMemoryFiles
|
|
13
13
|
} from './memory.ts';
|
|
14
14
|
import { getModels } from './models.ts';
|
|
15
|
-
import { updateModels } from './models-extended.ts';
|
|
15
|
+
import { setModel, updateModels } from './models-extended.ts';
|
|
16
16
|
import { listSessions } from './sessions.ts';
|
|
17
17
|
import { getSession, prepareMessage, attachSkill } from './sessions-extended.ts';
|
|
18
18
|
import {
|
|
@@ -57,6 +57,7 @@ const methods = new Map<string, MethodHandler>([
|
|
|
57
57
|
|
|
58
58
|
// Models
|
|
59
59
|
['models.get', getModels],
|
|
60
|
+
['models.set', setModel],
|
|
60
61
|
['models.update', updateModels],
|
|
61
62
|
|
|
62
63
|
// Usage
|
|
@@ -16,6 +16,7 @@ interface OpenClawConfig {
|
|
|
16
16
|
provider?: string;
|
|
17
17
|
[key: string]: any;
|
|
18
18
|
};
|
|
19
|
+
models?: Record<string, any>;
|
|
19
20
|
[key: string]: any;
|
|
20
21
|
};
|
|
21
22
|
[key: string]: any;
|
|
@@ -132,6 +133,67 @@ export const updateModels: MethodHandler = async (
|
|
|
132
133
|
};
|
|
133
134
|
};
|
|
134
135
|
|
|
136
|
+
export const setModel: MethodHandler = async (
|
|
137
|
+
params,
|
|
138
|
+
context
|
|
139
|
+
): Promise<JsonValue> => {
|
|
140
|
+
const objectParams = isObject(params) ? params : {};
|
|
141
|
+
const primary = pickString(objectParams.primary);
|
|
142
|
+
const provider = pickString(objectParams.provider);
|
|
143
|
+
|
|
144
|
+
if (!primary) {
|
|
145
|
+
throwModelError(
|
|
146
|
+
'MODEL_PRIMARY_REQUIRED',
|
|
147
|
+
'primary is required'
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const inferredProvider = inferProviderFromPrimaryModel(primary);
|
|
152
|
+
if (!inferredProvider) {
|
|
153
|
+
throwModelError(
|
|
154
|
+
'MODEL_PRIMARY_INVALID',
|
|
155
|
+
'primary must be in provider/model format'
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (provider && provider !== inferredProvider) {
|
|
160
|
+
throwModelError(
|
|
161
|
+
'MODEL_PROVIDER_MISMATCH',
|
|
162
|
+
'provider does not match primary'
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const configPath = path.join(context.openclawRoot, 'openclaw.json');
|
|
167
|
+
const config = await readJsonFile<OpenClawConfig>(configPath);
|
|
168
|
+
|
|
169
|
+
if (!isAllowedModel(config, primary)) {
|
|
170
|
+
throwModelError(
|
|
171
|
+
'MODEL_NOT_ALLOWED',
|
|
172
|
+
'model is not in allowed model options'
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (!config.agents) config.agents = {};
|
|
177
|
+
if (!config.agents.defaults) config.agents.defaults = {};
|
|
178
|
+
if (!config.agents.defaults.model) config.agents.defaults.model = {};
|
|
179
|
+
|
|
180
|
+
config.agents.defaults.model.primary = primary;
|
|
181
|
+
config.agents.defaults.model.provider = inferredProvider;
|
|
182
|
+
|
|
183
|
+
await writeJsonFile(configPath, config);
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
ok: true,
|
|
187
|
+
model: {
|
|
188
|
+
primary,
|
|
189
|
+
provider: inferredProvider
|
|
190
|
+
},
|
|
191
|
+
defaults: {
|
|
192
|
+
model: config.agents.defaults.model
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
};
|
|
196
|
+
|
|
135
197
|
function inferProviderFromPrimaryModel(primaryModel: string): string | null {
|
|
136
198
|
const slashIndex = primaryModel.indexOf('/');
|
|
137
199
|
if (slashIndex <= 0) {
|
|
@@ -141,6 +203,29 @@ function inferProviderFromPrimaryModel(primaryModel: string): string | null {
|
|
|
141
203
|
return primaryModel.slice(0, slashIndex);
|
|
142
204
|
}
|
|
143
205
|
|
|
206
|
+
function isAllowedModel(config: OpenClawConfig, primary: string): boolean {
|
|
207
|
+
const catalog = config.agents?.defaults?.models;
|
|
208
|
+
if (catalog && typeof catalog === 'object' && !Array.isArray(catalog)) {
|
|
209
|
+
return Object.prototype.hasOwnProperty.call(catalog, primary);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const currentPrimary = pickString(config.agents?.defaults?.model?.primary);
|
|
213
|
+
if (currentPrimary === primary) {
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const agents = Array.isArray(config.agents?.list) ? config.agents.list : [];
|
|
218
|
+
return agents.some((agent: any) => pickString(agent?.model?.primary) === primary);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function throwModelError(errorCode: string, message: string): never {
|
|
222
|
+
throw new JsonRpcException(
|
|
223
|
+
JSON_RPC_ERRORS.invalidParams,
|
|
224
|
+
message,
|
|
225
|
+
{ code: errorCode }
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
144
229
|
function pickString(value: JsonValue | undefined): string | null {
|
|
145
230
|
if (typeof value !== 'string') {
|
|
146
231
|
return null;
|
|
@@ -6,7 +6,7 @@ import type { JsonValue, MethodHandler } from '../types.ts';
|
|
|
6
6
|
interface OpenClawConfig {
|
|
7
7
|
models?: {
|
|
8
8
|
mode?: string;
|
|
9
|
-
providers?:
|
|
9
|
+
providers?: Record<string, any>;
|
|
10
10
|
};
|
|
11
11
|
agents?: {
|
|
12
12
|
defaults?: {
|
|
@@ -15,6 +15,7 @@ interface OpenClawConfig {
|
|
|
15
15
|
provider?: string;
|
|
16
16
|
[key: string]: any;
|
|
17
17
|
};
|
|
18
|
+
models?: Record<string, any>;
|
|
18
19
|
[key: string]: any;
|
|
19
20
|
};
|
|
20
21
|
list?: Array<{
|
|
@@ -33,15 +34,19 @@ interface OpenClawConfig {
|
|
|
33
34
|
export const getModels: MethodHandler = async (_params, context): Promise<JsonValue> => {
|
|
34
35
|
const configPath = path.join(context.openclawRoot, 'openclaw.json');
|
|
35
36
|
const config = await readJsonFile<OpenClawConfig>(configPath);
|
|
37
|
+
const agents = normalizeAgentModels(config);
|
|
38
|
+
const providers = config.models?.providers ?? {};
|
|
36
39
|
|
|
37
40
|
return {
|
|
38
41
|
sourceConfigFile: configPath,
|
|
39
42
|
defaults: {
|
|
40
|
-
model: config.agents?.defaults?.model
|
|
43
|
+
model: normalizeModelConfig(config.agents?.defaults?.model),
|
|
44
|
+
models: config.agents?.defaults?.models ?? {}
|
|
41
45
|
},
|
|
42
|
-
agents
|
|
46
|
+
agents,
|
|
47
|
+
modelOptions: buildModelOptions(config, providers, agents),
|
|
43
48
|
modelConfigMode: config.models?.mode ?? null,
|
|
44
|
-
configuredProviders: redactSecrets(
|
|
49
|
+
configuredProviders: redactSecrets(providers)
|
|
45
50
|
};
|
|
46
51
|
};
|
|
47
52
|
|
|
@@ -50,7 +55,7 @@ function normalizeAgentModels(config: OpenClawConfig): JsonValue[] {
|
|
|
50
55
|
{
|
|
51
56
|
id: 'defaults',
|
|
52
57
|
name: 'defaults',
|
|
53
|
-
model: config.agents?.defaults?.model
|
|
58
|
+
model: normalizeModelConfig(config.agents?.defaults?.model)
|
|
54
59
|
}
|
|
55
60
|
];
|
|
56
61
|
|
|
@@ -59,13 +64,188 @@ function normalizeAgentModels(config: OpenClawConfig): JsonValue[] {
|
|
|
59
64
|
items.push({
|
|
60
65
|
id: agent.id ?? null,
|
|
61
66
|
name: agent.name ?? agent.id ?? null,
|
|
62
|
-
model: agent.model
|
|
67
|
+
model: normalizeModelConfig(agent.model)
|
|
63
68
|
});
|
|
64
69
|
}
|
|
65
70
|
|
|
66
71
|
return items;
|
|
67
72
|
}
|
|
68
73
|
|
|
74
|
+
function normalizeModelConfig(modelConfig: any): JsonValue {
|
|
75
|
+
if (!modelConfig || typeof modelConfig !== 'object' || Array.isArray(modelConfig)) {
|
|
76
|
+
return {};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const primary = pickString(modelConfig.primary);
|
|
80
|
+
const provider = pickString(modelConfig.provider);
|
|
81
|
+
const inferredProvider = primary && !provider ? inferProviderFromPrimaryModel(primary) : null;
|
|
82
|
+
return {
|
|
83
|
+
...modelConfig,
|
|
84
|
+
...(inferredProvider ? { provider: inferredProvider } : {})
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function buildModelOptions(
|
|
89
|
+
config: OpenClawConfig,
|
|
90
|
+
providers: Record<string, any>,
|
|
91
|
+
agents: JsonValue[]
|
|
92
|
+
): JsonValue[] {
|
|
93
|
+
const options = new Map<string, JsonValue>();
|
|
94
|
+
addCatalogModelOptions(options, providers, config.agents?.defaults?.models);
|
|
95
|
+
if (options.size === 0) {
|
|
96
|
+
addFallbackAgentModelOptions(options, providers, agents);
|
|
97
|
+
}
|
|
98
|
+
return Array.from(options.values());
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function addCatalogModelOptions(
|
|
102
|
+
options: Map<string, JsonValue>,
|
|
103
|
+
providers: Record<string, any>,
|
|
104
|
+
catalog: Record<string, any> | undefined
|
|
105
|
+
): void {
|
|
106
|
+
if (!catalog || typeof catalog !== 'object' || Array.isArray(catalog)) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
for (const [modelId, modelConfig] of Object.entries(catalog)) {
|
|
111
|
+
const provider = inferProviderFromPrimaryModel(modelId);
|
|
112
|
+
const model = extractModelName(modelId, provider);
|
|
113
|
+
const providerConfig = provider ? providers[provider] : null;
|
|
114
|
+
const providerLabel = provider
|
|
115
|
+
? (pickString(providerConfig?.label) ?? pickString(providerConfig?.name) ?? humanizeProviderId(provider))
|
|
116
|
+
: null;
|
|
117
|
+
const label = modelConfig && typeof modelConfig === 'object'
|
|
118
|
+
? (pickString(modelConfig.alias) ?? pickString(modelConfig.label) ?? pickString(modelConfig.name) ?? humanizeModelId(model))
|
|
119
|
+
: humanizeModelId(model);
|
|
120
|
+
|
|
121
|
+
addModelOption(options, {
|
|
122
|
+
provider,
|
|
123
|
+
providerLabel,
|
|
124
|
+
model,
|
|
125
|
+
value: modelId,
|
|
126
|
+
label
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function addFallbackAgentModelOptions(
|
|
132
|
+
options: Map<string, JsonValue>,
|
|
133
|
+
providers: Record<string, any>,
|
|
134
|
+
agents: JsonValue[]
|
|
135
|
+
): void {
|
|
136
|
+
for (const agent of agents) {
|
|
137
|
+
if (!agent || typeof agent !== 'object' || Array.isArray(agent)) {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const modelConfig = agent.model;
|
|
142
|
+
if (!modelConfig || typeof modelConfig !== 'object' || Array.isArray(modelConfig)) {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const primary = pickString(modelConfig.primary);
|
|
147
|
+
if (!primary) {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const provider = pickString(modelConfig.provider) ?? inferProviderFromPrimaryModel(primary);
|
|
152
|
+
const model = extractModelName(primary, provider);
|
|
153
|
+
const providerConfig = provider ? providers[provider] : null;
|
|
154
|
+
const providerLabel = provider
|
|
155
|
+
? (pickString(providerConfig?.label) ?? pickString(providerConfig?.name) ?? humanizeProviderId(provider))
|
|
156
|
+
: null;
|
|
157
|
+
|
|
158
|
+
addModelOption(options, {
|
|
159
|
+
provider,
|
|
160
|
+
providerLabel,
|
|
161
|
+
model,
|
|
162
|
+
value: primary,
|
|
163
|
+
label: humanizeModelId(model)
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function addModelOption(
|
|
169
|
+
options: Map<string, JsonValue>,
|
|
170
|
+
input: {
|
|
171
|
+
provider: string | null;
|
|
172
|
+
providerLabel: string | null;
|
|
173
|
+
model: string;
|
|
174
|
+
label: string;
|
|
175
|
+
value?: string;
|
|
176
|
+
}
|
|
177
|
+
): void {
|
|
178
|
+
const value = input.value ?? (input.provider ? `${input.provider}/${input.model}` : input.model);
|
|
179
|
+
if (options.has(value)) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
options.set(value, {
|
|
184
|
+
label: input.label,
|
|
185
|
+
value,
|
|
186
|
+
provider: input.provider,
|
|
187
|
+
providerLabel: input.providerLabel,
|
|
188
|
+
model: input.model
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function inferProviderFromPrimaryModel(primaryModel: string): string | null {
|
|
193
|
+
const slashIndex = primaryModel.indexOf('/');
|
|
194
|
+
if (slashIndex <= 0) {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return primaryModel.slice(0, slashIndex);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function extractModelName(primaryModel: string, provider: string | null): string {
|
|
202
|
+
if (provider && primaryModel.startsWith(`${provider}/`)) {
|
|
203
|
+
return primaryModel.slice(provider.length + 1);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const slashIndex = primaryModel.indexOf('/');
|
|
207
|
+
return slashIndex >= 0 ? primaryModel.slice(slashIndex + 1) : primaryModel;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function pickString(value: unknown): string | null {
|
|
211
|
+
if (typeof value !== 'string') {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const trimmed = value.trim();
|
|
216
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function humanizeProviderId(value: string): string {
|
|
220
|
+
const knownLabels: Record<string, string> = {
|
|
221
|
+
openai: 'OpenAI',
|
|
222
|
+
anthropic: 'Anthropic',
|
|
223
|
+
openrouter: 'OpenRouter'
|
|
224
|
+
};
|
|
225
|
+
if (knownLabels[value]) {
|
|
226
|
+
return knownLabels[value];
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return value
|
|
230
|
+
.replace(/^custom[-_]/, '')
|
|
231
|
+
.split(/[-_.]+/)
|
|
232
|
+
.filter(Boolean)
|
|
233
|
+
.map(capitalizeWord)
|
|
234
|
+
.join(' ');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function humanizeModelId(value: string): string {
|
|
238
|
+
return value
|
|
239
|
+
.split(/[-_]+/)
|
|
240
|
+
.filter(Boolean)
|
|
241
|
+
.map((part) => (/^\d+(\.\d+)?$/.test(part) ? part : capitalizeWord(part)))
|
|
242
|
+
.join(' ');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function capitalizeWord(value: string): string {
|
|
246
|
+
return value.length > 0 ? `${value.charAt(0).toUpperCase()}${value.slice(1)}` : value;
|
|
247
|
+
}
|
|
248
|
+
|
|
69
249
|
function redactSecrets(value: JsonValue): JsonValue {
|
|
70
250
|
if (Array.isArray(value)) {
|
|
71
251
|
return value.map(redactSecrets);
|