rol-websocket-channel 1.0.0 → 1.0.2

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/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',
@@ -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
  */
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "rol-websocket-channel",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Unified OpenClaw plugin: MQTT Channel + Admin Bridge for remote management",
5
5
  "license": "MIT",
6
6
  "author": "nixgnehc",
package/readme.md CHANGED
@@ -1,7 +1,8 @@
1
1
 
2
2
 
3
3
  ```
4
- openclaw plugins install "c:\Users\admin\Desktop\openclaw-demo\rol-websocket-channel" --force
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?: JsonValue;
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: normalizeAgentModels(config),
46
+ agents,
47
+ modelOptions: buildModelOptions(config, providers, agents),
43
48
  modelConfigMode: config.models?.mode ?? null,
44
- configuredProviders: redactSecrets(config.models?.providers ?? {})
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);