ocuclaw 0.1.0
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/README.md +25 -0
- package/dist/config/runtime-config.js +165 -0
- package/dist/domain/activity-status-adapter.js +1041 -0
- package/dist/domain/conversation-state.js +516 -0
- package/dist/domain/debug-store.js +700 -0
- package/dist/domain/message-emoji-filter.js +249 -0
- package/dist/domain/readability-system-prompt.js +17 -0
- package/dist/even-ai/even-ai-endpoint.js +938 -0
- package/dist/even-ai/even-ai-model-hook.js +80 -0
- package/dist/even-ai/even-ai-router.js +98 -0
- package/dist/even-ai/even-ai-run-waiter.js +265 -0
- package/dist/even-ai/even-ai-settings-store.js +365 -0
- package/dist/gateway/gateway-bridge.js +175 -0
- package/dist/gateway/openclaw-client.js +1570 -0
- package/dist/index.js +38 -0
- package/dist/runtime/downstream-handler.js +2747 -0
- package/dist/runtime/downstream-server.js +1565 -0
- package/dist/runtime/ocuclaw-settings-store.js +237 -0
- package/dist/runtime/protocol-adapter.js +378 -0
- package/dist/runtime/relay-core.js +1977 -0
- package/dist/runtime/relay-service.js +146 -0
- package/dist/runtime/session-service.js +1026 -0
- package/dist/runtime/upstream-runtime.js +931 -0
- package/openclaw.plugin.json +95 -0
- package/package.json +36 -0
|
@@ -0,0 +1,2747 @@
|
|
|
1
|
+
import { normalizeEvenAiRoutingMode } from "../even-ai/even-ai-settings-store.js";
|
|
2
|
+
import {
|
|
3
|
+
normalizeOcuClawDefaultModel,
|
|
4
|
+
normalizeOcuClawDefaultThinking,
|
|
5
|
+
normalizeOcuClawSystemPrompt,
|
|
6
|
+
} from "./ocuclaw-settings-store.js";
|
|
7
|
+
|
|
8
|
+
// --- Factory ---
|
|
9
|
+
|
|
10
|
+
function normalizeLogger(logger) {
|
|
11
|
+
if (!logger || typeof logger !== "object") {
|
|
12
|
+
return console;
|
|
13
|
+
}
|
|
14
|
+
return {
|
|
15
|
+
info: typeof logger.info === "function" ? logger.info.bind(logger) : console.log,
|
|
16
|
+
warn: typeof logger.warn === "function" ? logger.warn.bind(logger) : console.warn,
|
|
17
|
+
error: typeof logger.error === "function" ? logger.error.bind(logger) : console.error,
|
|
18
|
+
debug:
|
|
19
|
+
typeof logger.debug === "function" ? logger.debug.bind(logger) : console.debug,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Create a downstream message handler.
|
|
25
|
+
*
|
|
26
|
+
* Transport-agnostic protocol logic for processing messages from
|
|
27
|
+
* downstream clients (Even App, commander.html). Consumed by relay.js.
|
|
28
|
+
*
|
|
29
|
+
* @param {object} opts
|
|
30
|
+
* @param {(id: string, text: string, sessionKey: string|null, attachment: object|null) => Promise} opts.onSend
|
|
31
|
+
* Forward a user message to the upstream OpenClaw agent.
|
|
32
|
+
* @param {(sender: string, text: string) => Array} opts.onSimulate
|
|
33
|
+
* Inject a fake message into conversation state; returns pages array.
|
|
34
|
+
* @param {(request: object) => Promise<object>|object} [opts.onSimulateStream]
|
|
35
|
+
* Inject deterministic streaming payload (relay-local), then finalize pages.
|
|
36
|
+
* @param {() => Promise<Array>} opts.onNewChat
|
|
37
|
+
* Clear conversation and reset the OpenClaw session; returns empty pages array.
|
|
38
|
+
* @param {() => Promise<{models: Array, fetchedAtMs: number, stale: boolean}>} [opts.onGetModelsCatalog]
|
|
39
|
+
* Return cached model catalog snapshot.
|
|
40
|
+
* @param {() => Promise<{skills: Array, fetchedAtMs: number, stale: boolean}>} [opts.onGetSkillsCatalog]
|
|
41
|
+
* Return cached skills catalog snapshot.
|
|
42
|
+
* @param {() => Promise<{models: Array, fetchedAtMs: number, stale: boolean}>} [opts.onGetSonioxModels]
|
|
43
|
+
* Return cached Soniox model snapshot.
|
|
44
|
+
* @param {() => Promise<object>} [opts.onGetSessionModelConfig]
|
|
45
|
+
* Return current session model controls.
|
|
46
|
+
* @param {(patch: object) => Promise<{status: string, error?: string}>} [opts.onSetSessionModelConfig]
|
|
47
|
+
* Patch current session model controls.
|
|
48
|
+
* @param {() => Promise<object>} [opts.onGetEvenAiSettings]
|
|
49
|
+
* Return current relay-owned Even AI settings.
|
|
50
|
+
* @param {() => Promise<{sessions: Array, dedicatedKey: string}>} [opts.onGetEvenAiSessions]
|
|
51
|
+
* Return the Even AI session-browser payload.
|
|
52
|
+
* @param {(patch: object) => Promise<{status: string, error?: string, settings?: object}>} [opts.onSetEvenAiSettings]
|
|
53
|
+
* Patch relay-owned Even AI settings.
|
|
54
|
+
* @param {() => Promise<object>} [opts.onGetOcuClawSettings]
|
|
55
|
+
* Return current relay-owned OcuClaw settings.
|
|
56
|
+
* @param {(patch: object) => Promise<{status: string, error?: string, settings?: object}>} [opts.onSetOcuClawSettings]
|
|
57
|
+
* Patch relay-owned OcuClaw settings.
|
|
58
|
+
* @param {(clientId: string, payload: object) => Promise<object>|object} [opts.onRequestSonioxTemporaryKey]
|
|
59
|
+
* Mint a short-lived Soniox temporary key for the provided voiceSessionId.
|
|
60
|
+
* @param {() => object} [opts.onGetStatus]
|
|
61
|
+
* Return the current relay status snapshot.
|
|
62
|
+
* @param {() => boolean} opts.isUpstreamConnected
|
|
63
|
+
* Returns true if the OpenClaw gateway connection is active.
|
|
64
|
+
* @param {(clientId: string, payload: object) => void} [opts.onEventDebug]
|
|
65
|
+
* Optional structured client debug-event callback.
|
|
66
|
+
* @returns {object} Handler instance
|
|
67
|
+
*/
|
|
68
|
+
function createDownstreamHandler(opts) {
|
|
69
|
+
const logger = normalizeLogger(opts.logger);
|
|
70
|
+
const externalDebugToolsEnabled = opts.externalDebugToolsEnabled !== false;
|
|
71
|
+
const onSend = opts.onSend;
|
|
72
|
+
const onSimulate = opts.onSimulate;
|
|
73
|
+
const onSimulateStream = opts.onSimulateStream || null;
|
|
74
|
+
const onNewChat = opts.onNewChat;
|
|
75
|
+
const onGetSessions = opts.onGetSessions;
|
|
76
|
+
const onSwitchSession = opts.onSwitchSession;
|
|
77
|
+
const onNewSession = opts.onNewSession;
|
|
78
|
+
const onSlashCommand = opts.onSlashCommand;
|
|
79
|
+
const onGetModelsCatalog = opts.onGetModelsCatalog;
|
|
80
|
+
const onGetSkillsCatalog = opts.onGetSkillsCatalog;
|
|
81
|
+
const onGetSonioxModels = opts.onGetSonioxModels || null;
|
|
82
|
+
const onGetSessionModelConfig = opts.onGetSessionModelConfig;
|
|
83
|
+
const onSetSessionModelConfig = opts.onSetSessionModelConfig;
|
|
84
|
+
const onGetEvenAiSettings = opts.onGetEvenAiSettings;
|
|
85
|
+
const onGetEvenAiSessions = opts.onGetEvenAiSessions;
|
|
86
|
+
const onSetEvenAiSettings = opts.onSetEvenAiSettings;
|
|
87
|
+
const onGetOcuClawSettings = opts.onGetOcuClawSettings;
|
|
88
|
+
const onSetOcuClawSettings = opts.onSetOcuClawSettings;
|
|
89
|
+
const onRequestSonioxTemporaryKey = opts.onRequestSonioxTemporaryKey || null;
|
|
90
|
+
const onGetStatus = opts.onGetStatus || null;
|
|
91
|
+
const isUpstreamConnected = opts.isUpstreamConnected;
|
|
92
|
+
const onConsoleLog = opts.onConsoleLog || null;
|
|
93
|
+
const onApprovalResolve = opts.onApprovalResolve || null;
|
|
94
|
+
const onDebugSet = opts.onDebugSet || null;
|
|
95
|
+
const onDebugDump = opts.onDebugDump || null;
|
|
96
|
+
const onEventDebug = opts.onEventDebug || null;
|
|
97
|
+
const onRemoteControl = opts.onRemoteControl || null;
|
|
98
|
+
const getSnapshotRevision = opts.getSnapshotRevision || null;
|
|
99
|
+
|
|
100
|
+
/** Client IDs subscribed to raw protocol frame forwarding. */
|
|
101
|
+
const protocolSubscribers = new Set();
|
|
102
|
+
const APPROVAL_DECISIONS = new Set(["allow-once", "allow-always", "deny"]);
|
|
103
|
+
const approvalResolveCacheTtlMs = Number.isFinite(opts.approvalResolveCacheTtlMs)
|
|
104
|
+
? Math.max(1_000, Math.floor(opts.approvalResolveCacheTtlMs))
|
|
105
|
+
: 30_000;
|
|
106
|
+
const approvalResolveCacheMaxEntries = Number.isFinite(opts.approvalResolveCacheMaxEntries)
|
|
107
|
+
? Math.max(10, Math.floor(opts.approvalResolveCacheMaxEntries))
|
|
108
|
+
: 500;
|
|
109
|
+
const EXTERNAL_DEBUG_TOOLS_DISABLED_ERROR =
|
|
110
|
+
"external debug tools are disabled by plugin config";
|
|
111
|
+
/** @type {Map<string, {expiresAtMs: number, promise: Promise<object>}>} */
|
|
112
|
+
const approvalResolveCache = new Map();
|
|
113
|
+
const APP_PROTOCOL = {
|
|
114
|
+
activity: "ocuclaw.activity.update",
|
|
115
|
+
approvalRequest: "ocuclaw.approval.request",
|
|
116
|
+
approvalResolve: "ocuclaw.approval.resolve",
|
|
117
|
+
approvalResolveAck: "ocuclaw.approval.resolve.ack",
|
|
118
|
+
approvalResolved: "ocuclaw.approval.resolved",
|
|
119
|
+
commandSlash: "ocuclaw.command.slash",
|
|
120
|
+
debugConfigSnapshot: "ocuclaw.debug.config.snapshot",
|
|
121
|
+
debugEvent: "ocuclaw.debug.event",
|
|
122
|
+
evenAiSettingsGet: "ocuclaw.evenai.settings.get",
|
|
123
|
+
evenAiSessionList: "ocuclaw.evenai.session.list",
|
|
124
|
+
evenAiSessionListResult: "ocuclaw.evenai.session.list.result",
|
|
125
|
+
evenAiSettingsSet: "ocuclaw.evenai.settings.set",
|
|
126
|
+
evenAiSettingsSetAck: "ocuclaw.evenai.settings.set.ack",
|
|
127
|
+
evenAiSettingsSnapshot: "ocuclaw.evenai.settings.snapshot",
|
|
128
|
+
ocuClawSettingsGet: "ocuclaw.settings.get",
|
|
129
|
+
ocuClawSettingsSet: "ocuclaw.settings.set",
|
|
130
|
+
ocuClawSettingsSetAck: "ocuclaw.settings.set.ack",
|
|
131
|
+
ocuClawSettingsSnapshot: "ocuclaw.settings.snapshot",
|
|
132
|
+
messageSend: "ocuclaw.message.send",
|
|
133
|
+
messageSendAck: "ocuclaw.message.send.ack",
|
|
134
|
+
messageStreamDelta: "ocuclaw.message.stream.delta",
|
|
135
|
+
modelCatalogGet: "ocuclaw.model.catalog.get",
|
|
136
|
+
modelCatalogSnapshot: "ocuclaw.model.catalog.snapshot",
|
|
137
|
+
skillsCatalogGet: "ocuclaw.skills.catalog.get",
|
|
138
|
+
skillsCatalogSnapshot: "ocuclaw.skills.catalog.snapshot",
|
|
139
|
+
pages: "ocuclaw.view.pages.snapshot",
|
|
140
|
+
protocolSubscribe: "ocuclaw.protocol.tap.subscribe",
|
|
141
|
+
protocolFrame: "ocuclaw.protocol.tap.frame",
|
|
142
|
+
remoteControl: "ocuclaw.remote.control",
|
|
143
|
+
requestSonioxTemporaryKey: "requestSonioxTemporaryKey",
|
|
144
|
+
sonioxModelsGet: "ocuclaw.voice.soniox.models.get",
|
|
145
|
+
sonioxModelsSnapshot: "ocuclaw.voice.soniox.models.snapshot",
|
|
146
|
+
sessionConfigGet: "ocuclaw.session.config.get",
|
|
147
|
+
sessionConfigSet: "ocuclaw.session.config.set",
|
|
148
|
+
sessionConfigSetAck: "ocuclaw.session.config.set.ack",
|
|
149
|
+
sessionConfigSnapshot: "ocuclaw.session.config.snapshot",
|
|
150
|
+
sessionCreate: "ocuclaw.session.create",
|
|
151
|
+
sessionList: "ocuclaw.session.list",
|
|
152
|
+
sessionListResult: "ocuclaw.session.list.result",
|
|
153
|
+
sessionReset: "ocuclaw.session.reset",
|
|
154
|
+
sessionSwitch: "ocuclaw.session.switch",
|
|
155
|
+
sessionSwitchApplied: "ocuclaw.session.switch.applied",
|
|
156
|
+
sonioxTemporaryKey: "sonioxTemporaryKey",
|
|
157
|
+
sonioxTemporaryKeyError: "sonioxTemporaryKeyError",
|
|
158
|
+
status: "ocuclaw.runtime.status",
|
|
159
|
+
statusGet: "ocuclaw.runtime.status.get",
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// --- Format helpers ---
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Format a pages message for broadcast.
|
|
166
|
+
*
|
|
167
|
+
* @param {Array<{content: string, subPage: [number,number]|null}>} pages
|
|
168
|
+
* @param {{revision?: number}} [meta]
|
|
169
|
+
* @returns {string} JSON string
|
|
170
|
+
*/
|
|
171
|
+
function formatPages(pages, meta) {
|
|
172
|
+
const msg = { type: APP_PROTOCOL.pages, pages };
|
|
173
|
+
const fallbackRevision = getSnapshotRevision
|
|
174
|
+
? getSnapshotRevision("pages")
|
|
175
|
+
: null;
|
|
176
|
+
const revision = Number.isFinite(meta && meta.revision)
|
|
177
|
+
? Math.floor(meta.revision)
|
|
178
|
+
: Number.isFinite(fallbackRevision)
|
|
179
|
+
? Math.floor(fallbackRevision)
|
|
180
|
+
: null;
|
|
181
|
+
if (revision !== null) {
|
|
182
|
+
msg.revision = revision;
|
|
183
|
+
}
|
|
184
|
+
return JSON.stringify(msg);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Format a status message for broadcast.
|
|
189
|
+
*
|
|
190
|
+
* @param {object} status - Status fields (openclaw, agent, session, etc.)
|
|
191
|
+
* @param {{revision?: number}} [meta]
|
|
192
|
+
* @returns {string} JSON string
|
|
193
|
+
*/
|
|
194
|
+
function formatStatus(status, meta) {
|
|
195
|
+
const msg = { ...status, type: APP_PROTOCOL.status };
|
|
196
|
+
const fallbackRevision = getSnapshotRevision
|
|
197
|
+
? getSnapshotRevision("status")
|
|
198
|
+
: null;
|
|
199
|
+
const revision = Number.isFinite(meta && meta.revision)
|
|
200
|
+
? Math.floor(meta.revision)
|
|
201
|
+
: Number.isFinite(fallbackRevision)
|
|
202
|
+
? Math.floor(fallbackRevision)
|
|
203
|
+
: null;
|
|
204
|
+
if (revision !== null) {
|
|
205
|
+
msg.revision = revision;
|
|
206
|
+
}
|
|
207
|
+
return JSON.stringify(msg);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Format an activity message for broadcast.
|
|
212
|
+
*
|
|
213
|
+
* @param {object} activity - Activity fields (state, tool, etc.)
|
|
214
|
+
* @returns {string} JSON string
|
|
215
|
+
*/
|
|
216
|
+
function formatActivity(activity) {
|
|
217
|
+
return JSON.stringify({ ...activity, type: APP_PROTOCOL.activity });
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Format an error message for unicast.
|
|
222
|
+
*
|
|
223
|
+
* @param {string} error
|
|
224
|
+
* @returns {string} JSON string
|
|
225
|
+
*/
|
|
226
|
+
function formatError(error) {
|
|
227
|
+
return JSON.stringify({
|
|
228
|
+
type: "error",
|
|
229
|
+
error: error || "Unknown error",
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function isExternalDebugToolMessageType(messageType) {
|
|
234
|
+
return (
|
|
235
|
+
messageType === "debug-set" ||
|
|
236
|
+
messageType === "debug-dump" ||
|
|
237
|
+
messageType === "remote-control"
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Format a send acknowledgement for unicast.
|
|
243
|
+
*
|
|
244
|
+
* @param {string} id - Message ID being acknowledged
|
|
245
|
+
* @param {string} status - "accepted" or "rejected"
|
|
246
|
+
* @param {string} [error] - Error message (for rejected)
|
|
247
|
+
* @param {string} [errorCode] - Structured error code (for deterministic client handling)
|
|
248
|
+
* @returns {string} JSON string
|
|
249
|
+
*/
|
|
250
|
+
function formatSendAck(id, status, error, errorCode) {
|
|
251
|
+
const msg = { type: APP_PROTOCOL.messageSendAck, requestId: id, status };
|
|
252
|
+
if (error !== undefined) {
|
|
253
|
+
msg.error = error;
|
|
254
|
+
}
|
|
255
|
+
if (errorCode !== undefined) {
|
|
256
|
+
msg.errorCode = errorCode;
|
|
257
|
+
}
|
|
258
|
+
return JSON.stringify(msg);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Format a protocol frame for forwarding to subscribers.
|
|
263
|
+
*
|
|
264
|
+
* @param {string} direction - "in" or "out"
|
|
265
|
+
* @param {object} frame - Raw protocol frame
|
|
266
|
+
* @returns {string} JSON string
|
|
267
|
+
*/
|
|
268
|
+
function formatProtocol(direction, frame) {
|
|
269
|
+
return JSON.stringify({
|
|
270
|
+
type: APP_PROTOCOL.protocolFrame,
|
|
271
|
+
direction,
|
|
272
|
+
frame,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Format a streaming text message for broadcast during agent runs.
|
|
278
|
+
*
|
|
279
|
+
* @param {string} text - Streaming assistant text
|
|
280
|
+
* @returns {string} JSON string
|
|
281
|
+
*/
|
|
282
|
+
function formatStreaming(text) {
|
|
283
|
+
return JSON.stringify({ type: APP_PROTOCOL.messageStreamDelta, text });
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Format a sessions list message for unicast response.
|
|
288
|
+
*
|
|
289
|
+
* @param {Array<{key: string, updatedAt: number, preview: string, firstUserMessage?: string}>} sessions
|
|
290
|
+
* @returns {string} JSON string
|
|
291
|
+
*/
|
|
292
|
+
function formatSessions(sessions) {
|
|
293
|
+
return JSON.stringify({ type: APP_PROTOCOL.sessionListResult, sessions });
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Format a session-switched confirmation for broadcast.
|
|
298
|
+
*
|
|
299
|
+
* @param {string} sessionKey
|
|
300
|
+
* @returns {string} JSON string
|
|
301
|
+
*/
|
|
302
|
+
function formatSessionSwitched(sessionKey) {
|
|
303
|
+
return JSON.stringify({
|
|
304
|
+
type: APP_PROTOCOL.sessionSwitchApplied,
|
|
305
|
+
sessionKey,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Format a model catalog snapshot for unicast.
|
|
311
|
+
*
|
|
312
|
+
* @param {{models: Array, fetchedAtMs: number, stale: boolean}} payload
|
|
313
|
+
* @returns {string}
|
|
314
|
+
*/
|
|
315
|
+
function formatModelsCatalog(payload) {
|
|
316
|
+
return JSON.stringify({
|
|
317
|
+
type: APP_PROTOCOL.modelCatalogSnapshot,
|
|
318
|
+
models: Array.isArray(payload && payload.models) ? payload.models : [],
|
|
319
|
+
fetchedAtMs:
|
|
320
|
+
Number.isFinite(payload && payload.fetchedAtMs)
|
|
321
|
+
? Math.floor(payload.fetchedAtMs)
|
|
322
|
+
: 0,
|
|
323
|
+
stale: !!(payload && payload.stale),
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Format a cached skills catalog snapshot for unicast.
|
|
329
|
+
*
|
|
330
|
+
* @param {{skills?: Array, fetchedAtMs?: number, stale?: boolean}} payload
|
|
331
|
+
* @returns {string}
|
|
332
|
+
*/
|
|
333
|
+
function formatSkillsCatalog(payload) {
|
|
334
|
+
return JSON.stringify({
|
|
335
|
+
type: APP_PROTOCOL.skillsCatalogSnapshot,
|
|
336
|
+
skills: Array.isArray(payload && payload.skills) ? payload.skills : [],
|
|
337
|
+
fetchedAtMs:
|
|
338
|
+
Number.isFinite(payload && payload.fetchedAtMs)
|
|
339
|
+
? Math.floor(payload.fetchedAtMs)
|
|
340
|
+
: Date.now(),
|
|
341
|
+
stale: !!(payload && payload.stale),
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Format a Soniox model snapshot for unicast.
|
|
347
|
+
*
|
|
348
|
+
* @param {{models: Array, fetchedAtMs: number, stale: boolean}} payload
|
|
349
|
+
* @returns {string}
|
|
350
|
+
*/
|
|
351
|
+
function formatSonioxModels(payload) {
|
|
352
|
+
return JSON.stringify({
|
|
353
|
+
type: APP_PROTOCOL.sonioxModelsSnapshot,
|
|
354
|
+
models: Array.isArray(payload && payload.models) ? payload.models : [],
|
|
355
|
+
fetchedAtMs:
|
|
356
|
+
Number.isFinite(payload && payload.fetchedAtMs)
|
|
357
|
+
? Math.floor(payload.fetchedAtMs)
|
|
358
|
+
: 0,
|
|
359
|
+
stale: !!(payload && payload.stale),
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Format current session model controls.
|
|
365
|
+
*
|
|
366
|
+
* @param {object} payload
|
|
367
|
+
* @returns {string}
|
|
368
|
+
*/
|
|
369
|
+
function formatSessionModelConfig(payload) {
|
|
370
|
+
return JSON.stringify({
|
|
371
|
+
type: APP_PROTOCOL.sessionConfigSnapshot,
|
|
372
|
+
sessionKey: (payload && payload.sessionKey) || "",
|
|
373
|
+
modelProvider:
|
|
374
|
+
payload && typeof payload.modelProvider === "string"
|
|
375
|
+
? payload.modelProvider
|
|
376
|
+
: null,
|
|
377
|
+
model:
|
|
378
|
+
payload && typeof payload.model === "string" ? payload.model : null,
|
|
379
|
+
thinkingLevel:
|
|
380
|
+
payload && typeof payload.thinkingLevel === "string"
|
|
381
|
+
? payload.thinkingLevel
|
|
382
|
+
: "",
|
|
383
|
+
reasoningLevel:
|
|
384
|
+
payload && typeof payload.reasoningLevel === "string"
|
|
385
|
+
? payload.reasoningLevel
|
|
386
|
+
: "off",
|
|
387
|
+
verboseLevel:
|
|
388
|
+
payload && typeof payload.verboseLevel === "string"
|
|
389
|
+
? payload.verboseLevel
|
|
390
|
+
: "off",
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Format a session model config write acknowledgement.
|
|
396
|
+
*
|
|
397
|
+
* @param {{status: string, error?: string}} payload
|
|
398
|
+
* @returns {string}
|
|
399
|
+
*/
|
|
400
|
+
function formatSessionModelConfigAck(payload) {
|
|
401
|
+
const out = {
|
|
402
|
+
type: APP_PROTOCOL.sessionConfigSetAck,
|
|
403
|
+
status:
|
|
404
|
+
payload && typeof payload.status === "string"
|
|
405
|
+
? payload.status
|
|
406
|
+
: "rejected",
|
|
407
|
+
};
|
|
408
|
+
if (payload && payload.error !== undefined) {
|
|
409
|
+
out.error = payload.error;
|
|
410
|
+
}
|
|
411
|
+
return JSON.stringify(out);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Format the current relay-owned Even AI settings snapshot.
|
|
416
|
+
*
|
|
417
|
+
* @param {object} payload
|
|
418
|
+
* @returns {string}
|
|
419
|
+
*/
|
|
420
|
+
function formatEvenAiSettings(payload) {
|
|
421
|
+
return JSON.stringify({
|
|
422
|
+
type: APP_PROTOCOL.evenAiSettingsSnapshot,
|
|
423
|
+
routingMode: normalizeEvenAiRoutingMode(
|
|
424
|
+
payload && typeof payload.routingMode === "string"
|
|
425
|
+
? payload.routingMode
|
|
426
|
+
: undefined,
|
|
427
|
+
),
|
|
428
|
+
systemPrompt:
|
|
429
|
+
payload && typeof payload.systemPrompt === "string"
|
|
430
|
+
? payload.systemPrompt
|
|
431
|
+
: "",
|
|
432
|
+
defaultModel:
|
|
433
|
+
payload && typeof payload.defaultModel === "string"
|
|
434
|
+
? payload.defaultModel
|
|
435
|
+
: "",
|
|
436
|
+
defaultThinking:
|
|
437
|
+
payload && typeof payload.defaultThinking === "string"
|
|
438
|
+
? payload.defaultThinking
|
|
439
|
+
: "",
|
|
440
|
+
listenEnabled: payload && payload.listenEnabled === true,
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Format an Even AI settings write acknowledgement.
|
|
446
|
+
*
|
|
447
|
+
* @param {{status: string, error?: string}} payload
|
|
448
|
+
* @returns {string}
|
|
449
|
+
*/
|
|
450
|
+
function formatEvenAiSettingsAck(payload) {
|
|
451
|
+
const out = {
|
|
452
|
+
type: APP_PROTOCOL.evenAiSettingsSetAck,
|
|
453
|
+
status:
|
|
454
|
+
payload && typeof payload.status === "string"
|
|
455
|
+
? payload.status
|
|
456
|
+
: "rejected",
|
|
457
|
+
};
|
|
458
|
+
if (payload && payload.error !== undefined) {
|
|
459
|
+
out.error = payload.error;
|
|
460
|
+
}
|
|
461
|
+
return JSON.stringify(out);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Format the current relay-owned OcuClaw settings snapshot.
|
|
466
|
+
*
|
|
467
|
+
* @param {object} payload
|
|
468
|
+
* @returns {string}
|
|
469
|
+
*/
|
|
470
|
+
function formatOcuClawSettings(payload) {
|
|
471
|
+
return JSON.stringify({
|
|
472
|
+
type: APP_PROTOCOL.ocuClawSettingsSnapshot,
|
|
473
|
+
systemPrompt: normalizeOcuClawSystemPrompt(
|
|
474
|
+
payload && typeof payload.systemPrompt === "string"
|
|
475
|
+
? payload.systemPrompt
|
|
476
|
+
: undefined,
|
|
477
|
+
),
|
|
478
|
+
defaultModel: normalizeOcuClawDefaultModel(
|
|
479
|
+
payload && typeof payload.defaultModel === "string"
|
|
480
|
+
? payload.defaultModel
|
|
481
|
+
: undefined,
|
|
482
|
+
),
|
|
483
|
+
defaultThinking: normalizeOcuClawDefaultThinking(
|
|
484
|
+
payload && typeof payload.defaultThinking === "string"
|
|
485
|
+
? payload.defaultThinking
|
|
486
|
+
: undefined,
|
|
487
|
+
),
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Format an OcuClaw settings write acknowledgement.
|
|
493
|
+
*
|
|
494
|
+
* @param {{status: string, error?: string}} payload
|
|
495
|
+
* @returns {string}
|
|
496
|
+
*/
|
|
497
|
+
function formatOcuClawSettingsAck(payload) {
|
|
498
|
+
const out = {
|
|
499
|
+
type: APP_PROTOCOL.ocuClawSettingsSetAck,
|
|
500
|
+
status:
|
|
501
|
+
payload && typeof payload.status === "string"
|
|
502
|
+
? payload.status
|
|
503
|
+
: "rejected",
|
|
504
|
+
};
|
|
505
|
+
if (payload && payload.error !== undefined) {
|
|
506
|
+
out.error = payload.error;
|
|
507
|
+
}
|
|
508
|
+
return JSON.stringify(out);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Format the Even AI session-browser result payload.
|
|
513
|
+
*
|
|
514
|
+
* @param {{sessions?: Array, dedicatedKey?: string}} payload
|
|
515
|
+
* @returns {string}
|
|
516
|
+
*/
|
|
517
|
+
function formatEvenAiSessions(payload) {
|
|
518
|
+
return JSON.stringify({
|
|
519
|
+
type: APP_PROTOCOL.evenAiSessionListResult,
|
|
520
|
+
sessions: Array.isArray(payload && payload.sessions) ? payload.sessions : [],
|
|
521
|
+
dedicatedKey:
|
|
522
|
+
payload && typeof payload.dedicatedKey === "string"
|
|
523
|
+
? payload.dedicatedKey
|
|
524
|
+
: "ocuclaw:even-ai",
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Format an exec approval request for broadcast.
|
|
530
|
+
*
|
|
531
|
+
* @param {object} data - Approval payload from gateway
|
|
532
|
+
* @returns {string} JSON string
|
|
533
|
+
*/
|
|
534
|
+
function formatApproval(data) {
|
|
535
|
+
const request = data && data.request ? data.request : {};
|
|
536
|
+
return JSON.stringify({
|
|
537
|
+
type: APP_PROTOCOL.approvalRequest,
|
|
538
|
+
id: data.id,
|
|
539
|
+
requestId:
|
|
540
|
+
(data && typeof data.requestId === "string" && data.requestId) ||
|
|
541
|
+
(request && typeof request.requestId === "string" && request.requestId) ||
|
|
542
|
+
null,
|
|
543
|
+
command: request.command || "",
|
|
544
|
+
cwd: request.cwd || null,
|
|
545
|
+
agentId: request.agentId || null,
|
|
546
|
+
host: request.host || null,
|
|
547
|
+
security: request.security || null,
|
|
548
|
+
ask: request.ask || null,
|
|
549
|
+
resolvedPath: request.resolvedPath || null,
|
|
550
|
+
sessionKey: request.sessionKey || null,
|
|
551
|
+
createdAtMs: data.createdAtMs || 0,
|
|
552
|
+
expiresAtMs: data.expiresAtMs || 0,
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Format an exec approval resolution for broadcast.
|
|
558
|
+
*
|
|
559
|
+
* @param {object} data - Resolution payload from gateway
|
|
560
|
+
* @returns {string} JSON string
|
|
561
|
+
*/
|
|
562
|
+
function formatApprovalResolved(data) {
|
|
563
|
+
return JSON.stringify({
|
|
564
|
+
type: APP_PROTOCOL.approvalResolved,
|
|
565
|
+
id: data.id,
|
|
566
|
+
requestId:
|
|
567
|
+
data && typeof data.requestId === "string" && data.requestId
|
|
568
|
+
? data.requestId
|
|
569
|
+
: null,
|
|
570
|
+
decision: data.decision || null,
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Format an approval-response acknowledgement for unicast.
|
|
576
|
+
*
|
|
577
|
+
* @param {object} data
|
|
578
|
+
* @returns {string}
|
|
579
|
+
*/
|
|
580
|
+
function formatApprovalResponseAck(data) {
|
|
581
|
+
return JSON.stringify({
|
|
582
|
+
type: APP_PROTOCOL.approvalResolveAck,
|
|
583
|
+
id: data && data.id ? data.id : null,
|
|
584
|
+
decision: data && data.decision ? data.decision : null,
|
|
585
|
+
requestId:
|
|
586
|
+
data && data.requestId !== undefined && data.requestId !== null
|
|
587
|
+
? data.requestId
|
|
588
|
+
: null,
|
|
589
|
+
status:
|
|
590
|
+
data && typeof data.status === "string" && data.status
|
|
591
|
+
? data.status
|
|
592
|
+
: "rejected",
|
|
593
|
+
code:
|
|
594
|
+
data && typeof data.code === "string" && data.code
|
|
595
|
+
? data.code
|
|
596
|
+
: null,
|
|
597
|
+
message:
|
|
598
|
+
data && typeof data.message === "string" && data.message
|
|
599
|
+
? data.message
|
|
600
|
+
: null,
|
|
601
|
+
idempotent:
|
|
602
|
+
data && data.idempotent !== undefined
|
|
603
|
+
? !!data.idempotent
|
|
604
|
+
: false,
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Format a transcription update for broadcast during voice mode.
|
|
610
|
+
*
|
|
611
|
+
* @param {string} text - Current transcription text
|
|
612
|
+
* @param {boolean} isFinal - Whether this is a final transcription
|
|
613
|
+
* @returns {string} JSON string
|
|
614
|
+
*/
|
|
615
|
+
function formatTranscription(text, isFinal) {
|
|
616
|
+
return JSON.stringify({ type: "transcription", text, final: isFinal });
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Format a committed listen handoff update for broadcast during voice mode.
|
|
621
|
+
*
|
|
622
|
+
* @param {string} text - Final committed text sent upstream
|
|
623
|
+
* @param {"manual"|"endpoint"} source - Commit source path
|
|
624
|
+
* @param {string|null} sessionKey - Active relay session key for diagnostics
|
|
625
|
+
* @returns {string} JSON string
|
|
626
|
+
*/
|
|
627
|
+
function formatListenCommitted(text, source, sessionKey) {
|
|
628
|
+
return JSON.stringify({
|
|
629
|
+
type: "listen-committed",
|
|
630
|
+
text,
|
|
631
|
+
source,
|
|
632
|
+
sessionKey: sessionKey || null,
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Format a listen-ended message for broadcast.
|
|
638
|
+
* @returns {string} JSON string
|
|
639
|
+
*/
|
|
640
|
+
function formatListenEnded() {
|
|
641
|
+
return JSON.stringify({ type: "listen-ended" });
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Format a listen-error message for broadcast.
|
|
646
|
+
* @param {string} error - Error description
|
|
647
|
+
* @param {string|null} [code] - Structured error code
|
|
648
|
+
* @returns {string} JSON string
|
|
649
|
+
*/
|
|
650
|
+
function formatListenError(error, code = null) {
|
|
651
|
+
const msg = { type: "listen-error", error };
|
|
652
|
+
if (typeof code === "string" && code.trim()) {
|
|
653
|
+
msg.code = code.trim();
|
|
654
|
+
}
|
|
655
|
+
return JSON.stringify(msg);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Format a listen-ready message for broadcast (agent done, client can auto-restart).
|
|
660
|
+
* @returns {string} JSON string
|
|
661
|
+
*/
|
|
662
|
+
function formatListenReady() {
|
|
663
|
+
return JSON.stringify({ type: "listen-ready" });
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Format a temporary Soniox key lease for unicast back to the requesting client.
|
|
668
|
+
*
|
|
669
|
+
* @param {{voiceSessionId: string, temporaryKey: string, expiresAtMs: number}} payload
|
|
670
|
+
* @returns {string}
|
|
671
|
+
*/
|
|
672
|
+
function formatSonioxTemporaryKey(payload) {
|
|
673
|
+
return JSON.stringify({
|
|
674
|
+
type: APP_PROTOCOL.sonioxTemporaryKey,
|
|
675
|
+
voiceSessionId:
|
|
676
|
+
payload && typeof payload.voiceSessionId === "string"
|
|
677
|
+
? payload.voiceSessionId
|
|
678
|
+
: "",
|
|
679
|
+
temporaryKey:
|
|
680
|
+
payload && typeof payload.temporaryKey === "string"
|
|
681
|
+
? payload.temporaryKey
|
|
682
|
+
: "",
|
|
683
|
+
expiresAtMs:
|
|
684
|
+
payload && Number.isFinite(payload.expiresAtMs)
|
|
685
|
+
? Math.floor(payload.expiresAtMs)
|
|
686
|
+
: 0,
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Format a Soniox temporary-key failure for unicast back to the requesting client.
|
|
692
|
+
*
|
|
693
|
+
* @param {{voiceSessionId: string, error: string, code?: string|null}} payload
|
|
694
|
+
* @returns {string}
|
|
695
|
+
*/
|
|
696
|
+
function formatSonioxTemporaryKeyError(payload) {
|
|
697
|
+
const msg = {
|
|
698
|
+
type: APP_PROTOCOL.sonioxTemporaryKeyError,
|
|
699
|
+
voiceSessionId:
|
|
700
|
+
payload && typeof payload.voiceSessionId === "string"
|
|
701
|
+
? payload.voiceSessionId
|
|
702
|
+
: "",
|
|
703
|
+
error:
|
|
704
|
+
payload && typeof payload.error === "string" && payload.error.trim()
|
|
705
|
+
? payload.error.trim()
|
|
706
|
+
: "Soniox temporary-key request failed",
|
|
707
|
+
};
|
|
708
|
+
const code =
|
|
709
|
+
payload && typeof payload.code === "string" && payload.code.trim()
|
|
710
|
+
? payload.code.trim()
|
|
711
|
+
: "";
|
|
712
|
+
if (code) {
|
|
713
|
+
msg.code = code;
|
|
714
|
+
}
|
|
715
|
+
return JSON.stringify(msg);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function normalizeSonioxTemporaryKeyErrorCode(err) {
|
|
719
|
+
const message =
|
|
720
|
+
err && typeof err.message === "string" && err.message.trim()
|
|
721
|
+
? err.message.trim()
|
|
722
|
+
: "";
|
|
723
|
+
const lowered = message.toLowerCase();
|
|
724
|
+
if (!message) return "soniox_temp_key_request_failed";
|
|
725
|
+
if (lowered.includes("is not available")) {
|
|
726
|
+
return "soniox_temp_key_unavailable";
|
|
727
|
+
}
|
|
728
|
+
if (lowered.includes("voicesessionid is required")) {
|
|
729
|
+
return "soniox_temp_key_invalid_request";
|
|
730
|
+
}
|
|
731
|
+
if (lowered.includes("api key is not configured")) {
|
|
732
|
+
return "soniox_temp_key_not_configured";
|
|
733
|
+
}
|
|
734
|
+
if (lowered.includes("fetch is not available")) {
|
|
735
|
+
return "soniox_temp_key_fetch_unavailable";
|
|
736
|
+
}
|
|
737
|
+
if (lowered.includes("missing temporarykey") || lowered.includes("missing expiresatms")) {
|
|
738
|
+
return "soniox_temp_key_invalid_response";
|
|
739
|
+
}
|
|
740
|
+
const statusMatch = lowered.match(/\((\d{3})\)/);
|
|
741
|
+
if (statusMatch) {
|
|
742
|
+
return `soniox_temp_key_http_${statusMatch[1]}`;
|
|
743
|
+
}
|
|
744
|
+
return "soniox_temp_key_request_failed";
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Format a debug-set response for unicast.
|
|
749
|
+
*
|
|
750
|
+
* @param {object} data
|
|
751
|
+
* @returns {string}
|
|
752
|
+
*/
|
|
753
|
+
function formatDebugSet(data) {
|
|
754
|
+
return JSON.stringify({
|
|
755
|
+
type: "debug-set",
|
|
756
|
+
...data,
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* Format a debug-dump response for unicast.
|
|
762
|
+
*
|
|
763
|
+
* @param {object} data
|
|
764
|
+
* @returns {string}
|
|
765
|
+
*/
|
|
766
|
+
function formatDebugDump(data) {
|
|
767
|
+
return JSON.stringify({
|
|
768
|
+
type: "debug-dump",
|
|
769
|
+
...data,
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* Format the current relay debug-config snapshot for app/WebUI clients.
|
|
775
|
+
*
|
|
776
|
+
* @param {{serverNowMs: number, enabled: Array<{cat: string, expiresAtMs: number}>}} data
|
|
777
|
+
* @returns {string}
|
|
778
|
+
*/
|
|
779
|
+
function formatDebugConfigSnapshot(data) {
|
|
780
|
+
const enabled = Array.isArray(data && data.enabled)
|
|
781
|
+
? data.enabled
|
|
782
|
+
.filter(
|
|
783
|
+
(entry) =>
|
|
784
|
+
entry &&
|
|
785
|
+
typeof entry.cat === "string" &&
|
|
786
|
+
entry.cat.trim() &&
|
|
787
|
+
Number.isFinite(Number(entry.expiresAtMs)),
|
|
788
|
+
)
|
|
789
|
+
.map((entry) => ({
|
|
790
|
+
cat: entry.cat.trim(),
|
|
791
|
+
expiresAtMs: Math.floor(Number(entry.expiresAtMs)),
|
|
792
|
+
}))
|
|
793
|
+
: [];
|
|
794
|
+
return JSON.stringify({
|
|
795
|
+
type: APP_PROTOCOL.debugConfigSnapshot,
|
|
796
|
+
serverNowMs:
|
|
797
|
+
Number.isFinite(data && data.serverNowMs)
|
|
798
|
+
? Math.floor(data.serverNowMs)
|
|
799
|
+
: 0,
|
|
800
|
+
enabled,
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* Format a remote-control command for broadcast to app clients.
|
|
806
|
+
*
|
|
807
|
+
* @param {object} data
|
|
808
|
+
* @returns {string}
|
|
809
|
+
*/
|
|
810
|
+
function formatRemoteControl(data) {
|
|
811
|
+
return JSON.stringify({
|
|
812
|
+
type: APP_PROTOCOL.remoteControl,
|
|
813
|
+
...data,
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* Format a remote-control acknowledgement for unicast.
|
|
819
|
+
*
|
|
820
|
+
* @param {object} data
|
|
821
|
+
* @returns {string}
|
|
822
|
+
*/
|
|
823
|
+
function formatRemoteControlAck(data) {
|
|
824
|
+
return JSON.stringify({
|
|
825
|
+
type: "remote-control-ack",
|
|
826
|
+
...data,
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function normalizeCategories(raw, fieldName) {
|
|
831
|
+
if (raw === undefined || raw === null) return [];
|
|
832
|
+
if (!Array.isArray(raw)) {
|
|
833
|
+
throw new Error(`${fieldName} must be an array`);
|
|
834
|
+
}
|
|
835
|
+
const dedup = new Set();
|
|
836
|
+
for (const entry of raw) {
|
|
837
|
+
if (typeof entry !== "string") {
|
|
838
|
+
throw new Error(`${fieldName} entries must be strings`);
|
|
839
|
+
}
|
|
840
|
+
const cat = entry.trim();
|
|
841
|
+
if (!cat) {
|
|
842
|
+
throw new Error(`${fieldName} entries must be non-empty strings`);
|
|
843
|
+
}
|
|
844
|
+
dedup.add(cat);
|
|
845
|
+
}
|
|
846
|
+
return Array.from(dedup.values());
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function parseDebugSet(msg) {
|
|
850
|
+
const hasEnableDisable = msg.enable !== undefined || msg.disable !== undefined;
|
|
851
|
+
const enable = hasEnableDisable
|
|
852
|
+
? normalizeCategories(msg.enable, "enable")
|
|
853
|
+
: msg.enabled === false
|
|
854
|
+
? []
|
|
855
|
+
: normalizeCategories(msg.categories, "categories");
|
|
856
|
+
const disable = hasEnableDisable
|
|
857
|
+
? normalizeCategories(msg.disable, "disable")
|
|
858
|
+
: msg.enabled === false
|
|
859
|
+
? normalizeCategories(msg.categories, "categories")
|
|
860
|
+
: [];
|
|
861
|
+
if (enable.length === 0 && disable.length === 0) {
|
|
862
|
+
throw new Error("debug-set requires categories to enable and/or disable");
|
|
863
|
+
}
|
|
864
|
+
if (
|
|
865
|
+
msg.ttlMs !== undefined &&
|
|
866
|
+
(!Number.isFinite(Number(msg.ttlMs)) || Number(msg.ttlMs) <= 0)
|
|
867
|
+
) {
|
|
868
|
+
throw new Error("debug-set ttlMs must be a positive number");
|
|
869
|
+
}
|
|
870
|
+
return {
|
|
871
|
+
enable,
|
|
872
|
+
disable,
|
|
873
|
+
ttlMs: msg.ttlMs === undefined ? undefined : Number(msg.ttlMs),
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
function parseDebugDump(msg) {
|
|
878
|
+
const categories = normalizeCategories(msg.categories, "categories");
|
|
879
|
+
if (
|
|
880
|
+
msg.limit !== undefined &&
|
|
881
|
+
(!Number.isFinite(Number(msg.limit)) || Number(msg.limit) <= 0)
|
|
882
|
+
) {
|
|
883
|
+
throw new Error("debug-dump limit must be a positive number");
|
|
884
|
+
}
|
|
885
|
+
if (
|
|
886
|
+
msg.sinceMs !== undefined &&
|
|
887
|
+
(!Number.isFinite(Number(msg.sinceMs)) || Number(msg.sinceMs) < 0)
|
|
888
|
+
) {
|
|
889
|
+
throw new Error("debug-dump sinceMs must be a non-negative number");
|
|
890
|
+
}
|
|
891
|
+
if (
|
|
892
|
+
msg.sinceAgeMs !== undefined &&
|
|
893
|
+
(!Number.isFinite(Number(msg.sinceAgeMs)) || Number(msg.sinceAgeMs) < 0)
|
|
894
|
+
) {
|
|
895
|
+
throw new Error("debug-dump sinceAgeMs must be a non-negative number");
|
|
896
|
+
}
|
|
897
|
+
if (
|
|
898
|
+
msg.untilMs !== undefined &&
|
|
899
|
+
(!Number.isFinite(Number(msg.untilMs)) || Number(msg.untilMs) < 0)
|
|
900
|
+
) {
|
|
901
|
+
throw new Error("debug-dump untilMs must be a non-negative number");
|
|
902
|
+
}
|
|
903
|
+
if (msg.redaction !== undefined) {
|
|
904
|
+
if (typeof msg.redaction !== "string") {
|
|
905
|
+
throw new Error("debug-dump redaction must be one of: safe, full");
|
|
906
|
+
}
|
|
907
|
+
const normalized = msg.redaction.trim().toLowerCase();
|
|
908
|
+
if (normalized !== "safe" && normalized !== "full") {
|
|
909
|
+
throw new Error("debug-dump redaction must be one of: safe, full");
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
return {
|
|
913
|
+
categories,
|
|
914
|
+
limit: msg.limit === undefined ? undefined : Number(msg.limit),
|
|
915
|
+
sinceMs: msg.sinceMs === undefined ? undefined : Number(msg.sinceMs),
|
|
916
|
+
sinceAgeMs: msg.sinceAgeMs === undefined ? undefined : Number(msg.sinceAgeMs),
|
|
917
|
+
untilMs: msg.untilMs === undefined ? undefined : Number(msg.untilMs),
|
|
918
|
+
redaction:
|
|
919
|
+
msg.redaction === undefined
|
|
920
|
+
? undefined
|
|
921
|
+
: msg.redaction.trim().toLowerCase(),
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
function parseEventDebug(msg) {
|
|
926
|
+
if (!msg || typeof msg !== "object") return null;
|
|
927
|
+
if (typeof msg.cat !== "string" || !msg.cat.trim()) return null;
|
|
928
|
+
if (typeof msg.event !== "string" || !msg.event.trim()) return null;
|
|
929
|
+
const severity =
|
|
930
|
+
msg.severity === "info" ||
|
|
931
|
+
msg.severity === "warn" ||
|
|
932
|
+
msg.severity === "error"
|
|
933
|
+
? msg.severity
|
|
934
|
+
: "debug";
|
|
935
|
+
return {
|
|
936
|
+
cat: msg.cat.trim(),
|
|
937
|
+
event: msg.event.trim(),
|
|
938
|
+
severity,
|
|
939
|
+
screen:
|
|
940
|
+
typeof msg.screen === "string" && msg.screen.trim()
|
|
941
|
+
? msg.screen.trim()
|
|
942
|
+
: null,
|
|
943
|
+
runId:
|
|
944
|
+
typeof msg.runId === "string" && msg.runId.trim()
|
|
945
|
+
? msg.runId.trim()
|
|
946
|
+
: null,
|
|
947
|
+
sessionKey:
|
|
948
|
+
typeof msg.sessionKey === "string" && msg.sessionKey.trim()
|
|
949
|
+
? msg.sessionKey.trim()
|
|
950
|
+
: null,
|
|
951
|
+
data:
|
|
952
|
+
msg.data && typeof msg.data === "object" && !Array.isArray(msg.data)
|
|
953
|
+
? msg.data
|
|
954
|
+
: { value: msg.data ?? null },
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
function normalizeRemoteButton(raw) {
|
|
959
|
+
if (typeof raw !== "string" || !raw.trim()) {
|
|
960
|
+
throw new Error("remote-control button is required");
|
|
961
|
+
}
|
|
962
|
+
const normalized = raw.trim().toLowerCase();
|
|
963
|
+
if (
|
|
964
|
+
normalized === "click" ||
|
|
965
|
+
normalized === "tap" ||
|
|
966
|
+
normalized === "double-click" ||
|
|
967
|
+
normalized === "double_click" ||
|
|
968
|
+
normalized === "doubleclick" ||
|
|
969
|
+
normalized === "double-tap" ||
|
|
970
|
+
normalized === "double_tap" ||
|
|
971
|
+
normalized === "doubletap" ||
|
|
972
|
+
normalized === "scroll-up" ||
|
|
973
|
+
normalized === "scroll_up" ||
|
|
974
|
+
normalized === "scrollup" ||
|
|
975
|
+
normalized === "up" ||
|
|
976
|
+
normalized === "scroll-down" ||
|
|
977
|
+
normalized === "scroll_down" ||
|
|
978
|
+
normalized === "scrolldown" ||
|
|
979
|
+
normalized === "down"
|
|
980
|
+
) {
|
|
981
|
+
if (normalized === "tap") return "click";
|
|
982
|
+
if (
|
|
983
|
+
normalized === "double_click" ||
|
|
984
|
+
normalized === "doubleclick" ||
|
|
985
|
+
normalized === "double-tap" ||
|
|
986
|
+
normalized === "double_tap" ||
|
|
987
|
+
normalized === "doubletap"
|
|
988
|
+
) {
|
|
989
|
+
return "double-click";
|
|
990
|
+
}
|
|
991
|
+
if (normalized === "scroll_up" || normalized === "scrollup" || normalized === "up") {
|
|
992
|
+
return "scroll-up";
|
|
993
|
+
}
|
|
994
|
+
if (normalized === "scroll_down" || normalized === "scrolldown" || normalized === "down") {
|
|
995
|
+
return "scroll-down";
|
|
996
|
+
}
|
|
997
|
+
return normalized;
|
|
998
|
+
}
|
|
999
|
+
throw new Error(`unsupported remote-control button: ${raw}`);
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
function normalizeRemoteRelayAction(raw) {
|
|
1003
|
+
if (typeof raw !== "string" || !raw.trim()) {
|
|
1004
|
+
throw new Error("remote-control relayAction is required");
|
|
1005
|
+
}
|
|
1006
|
+
const normalized = raw.trim().toLowerCase();
|
|
1007
|
+
if (
|
|
1008
|
+
normalized === "perf-conversation-upgrade-probe" ||
|
|
1009
|
+
normalized === "perf_conversation_upgrade_probe" ||
|
|
1010
|
+
normalized === "perfconversationupgradeprobe" ||
|
|
1011
|
+
normalized === "conversation-upgrade-probe" ||
|
|
1012
|
+
normalized === "conversation_upgrade_probe" ||
|
|
1013
|
+
normalized === "conversationupgradeprobe"
|
|
1014
|
+
) {
|
|
1015
|
+
return "perf-conversation-upgrade-probe";
|
|
1016
|
+
}
|
|
1017
|
+
if (
|
|
1018
|
+
normalized === "new-session" ||
|
|
1019
|
+
normalized === "new_session" ||
|
|
1020
|
+
normalized === "newsession"
|
|
1021
|
+
) {
|
|
1022
|
+
return "new-session";
|
|
1023
|
+
}
|
|
1024
|
+
if (
|
|
1025
|
+
normalized === "get-sessions" ||
|
|
1026
|
+
normalized === "get_sessions" ||
|
|
1027
|
+
normalized === "getsessions" ||
|
|
1028
|
+
normalized === "sessions"
|
|
1029
|
+
) {
|
|
1030
|
+
return "get-sessions";
|
|
1031
|
+
}
|
|
1032
|
+
if (
|
|
1033
|
+
normalized === "switch-session" ||
|
|
1034
|
+
normalized === "switch_session" ||
|
|
1035
|
+
normalized === "switchsession"
|
|
1036
|
+
) {
|
|
1037
|
+
return "switch-session";
|
|
1038
|
+
}
|
|
1039
|
+
if (normalized === "new-chat" || normalized === "new_chat" || normalized === "newchat") {
|
|
1040
|
+
return "new-chat";
|
|
1041
|
+
}
|
|
1042
|
+
if (
|
|
1043
|
+
normalized === "slash-command" ||
|
|
1044
|
+
normalized === "slash_command" ||
|
|
1045
|
+
normalized === "slash"
|
|
1046
|
+
) {
|
|
1047
|
+
return "slash-command";
|
|
1048
|
+
}
|
|
1049
|
+
if (
|
|
1050
|
+
normalized === "listen-start" ||
|
|
1051
|
+
normalized === "listen_start" ||
|
|
1052
|
+
normalized === "listenstart"
|
|
1053
|
+
) {
|
|
1054
|
+
return "listen-start";
|
|
1055
|
+
}
|
|
1056
|
+
if (
|
|
1057
|
+
normalized === "listen-stop" ||
|
|
1058
|
+
normalized === "listen_stop" ||
|
|
1059
|
+
normalized === "listenstop"
|
|
1060
|
+
) {
|
|
1061
|
+
return "listen-stop";
|
|
1062
|
+
}
|
|
1063
|
+
if (
|
|
1064
|
+
normalized === "listen-send" ||
|
|
1065
|
+
normalized === "listen_send" ||
|
|
1066
|
+
normalized === "listensend"
|
|
1067
|
+
) {
|
|
1068
|
+
return "listen-send";
|
|
1069
|
+
}
|
|
1070
|
+
if (
|
|
1071
|
+
normalized === "listen-retry" ||
|
|
1072
|
+
normalized === "listen_retry" ||
|
|
1073
|
+
normalized === "listenretry"
|
|
1074
|
+
) {
|
|
1075
|
+
return "listen-retry";
|
|
1076
|
+
}
|
|
1077
|
+
if (
|
|
1078
|
+
normalized === "perf-reset-ladder" ||
|
|
1079
|
+
normalized === "perf_reset_ladder" ||
|
|
1080
|
+
normalized === "perfresetladder" ||
|
|
1081
|
+
normalized === "reset-ladder" ||
|
|
1082
|
+
normalized === "reset_ladder" ||
|
|
1083
|
+
normalized === "resetladder"
|
|
1084
|
+
) {
|
|
1085
|
+
return "perf-reset-ladder";
|
|
1086
|
+
}
|
|
1087
|
+
if (
|
|
1088
|
+
normalized === "perf-relay-reconnect-only" ||
|
|
1089
|
+
normalized === "perf_relay_reconnect_only" ||
|
|
1090
|
+
normalized === "perfrelayreconnectonly" ||
|
|
1091
|
+
normalized === "relay-reconnect-only" ||
|
|
1092
|
+
normalized === "relay_reconnect_only" ||
|
|
1093
|
+
normalized === "relayreconnectonly"
|
|
1094
|
+
) {
|
|
1095
|
+
return "perf-relay-reconnect-only";
|
|
1096
|
+
}
|
|
1097
|
+
if (
|
|
1098
|
+
normalized === "perf-sdk-page-recreate-only" ||
|
|
1099
|
+
normalized === "perf_sdk_page_recreate_only" ||
|
|
1100
|
+
normalized === "perfsdkpagerecreateonly" ||
|
|
1101
|
+
normalized === "sdk-page-recreate-only" ||
|
|
1102
|
+
normalized === "sdk_page_recreate_only" ||
|
|
1103
|
+
normalized === "sdkpagerecreateonly"
|
|
1104
|
+
) {
|
|
1105
|
+
return "perf-sdk-page-recreate-only";
|
|
1106
|
+
}
|
|
1107
|
+
if (
|
|
1108
|
+
normalized === "perf-config" ||
|
|
1109
|
+
normalized === "perf_config" ||
|
|
1110
|
+
normalized === "perfconfig" ||
|
|
1111
|
+
normalized === "perf-drift-config" ||
|
|
1112
|
+
normalized === "perf_drift_config" ||
|
|
1113
|
+
normalized === "perfdriftconfig"
|
|
1114
|
+
) {
|
|
1115
|
+
return "perf-config";
|
|
1116
|
+
}
|
|
1117
|
+
if (
|
|
1118
|
+
normalized === "debug-close-app-client" ||
|
|
1119
|
+
normalized === "debug_close_app_client" ||
|
|
1120
|
+
normalized === "debugcloseappclient" ||
|
|
1121
|
+
normalized === "close-app-client" ||
|
|
1122
|
+
normalized === "close_app_client" ||
|
|
1123
|
+
normalized === "closeappclient"
|
|
1124
|
+
) {
|
|
1125
|
+
return "debug-close-app-client";
|
|
1126
|
+
}
|
|
1127
|
+
throw new Error(`unsupported remote relayAction: ${raw}`);
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
function parseOptionalTrimmedString(raw) {
|
|
1131
|
+
if (typeof raw !== "string") return null;
|
|
1132
|
+
const trimmed = raw.trim();
|
|
1133
|
+
return trimmed || null;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
function parseOptionalBoolean(raw, fieldName) {
|
|
1137
|
+
if (raw === undefined || raw === null) return undefined;
|
|
1138
|
+
if (typeof raw === "boolean") return raw;
|
|
1139
|
+
if (typeof raw === "number") return raw !== 0;
|
|
1140
|
+
if (typeof raw !== "string") {
|
|
1141
|
+
throw new Error(`${fieldName} must be a boolean`);
|
|
1142
|
+
}
|
|
1143
|
+
const normalized = raw.trim().toLowerCase();
|
|
1144
|
+
if (["true", "1", "yes", "y", "on"].includes(normalized)) return true;
|
|
1145
|
+
if (["false", "0", "no", "n", "off"].includes(normalized)) return false;
|
|
1146
|
+
throw new Error(`${fieldName} must be a boolean`);
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
function parseOptionalPositiveNumber(raw, fieldName) {
|
|
1150
|
+
if (raw === undefined || raw === null) return undefined;
|
|
1151
|
+
const num = Number(raw);
|
|
1152
|
+
if (!Number.isFinite(num) || num <= 0) {
|
|
1153
|
+
throw new Error(`${fieldName} must be a positive number`);
|
|
1154
|
+
}
|
|
1155
|
+
return Math.floor(num);
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
function parseOptionalNonNegativeNumber(raw, fieldName) {
|
|
1159
|
+
if (raw === undefined || raw === null) return undefined;
|
|
1160
|
+
const num = Number(raw);
|
|
1161
|
+
if (!Number.isFinite(num) || num < 0) {
|
|
1162
|
+
throw new Error(`${fieldName} must be a non-negative number`);
|
|
1163
|
+
}
|
|
1164
|
+
return Math.floor(num);
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
function parseOptionalStringArray(raw, fieldName) {
|
|
1168
|
+
if (raw === undefined || raw === null) return undefined;
|
|
1169
|
+
if (!Array.isArray(raw)) {
|
|
1170
|
+
throw new Error(`${fieldName} must be an array of strings`);
|
|
1171
|
+
}
|
|
1172
|
+
const values = [];
|
|
1173
|
+
for (const entry of raw) {
|
|
1174
|
+
if (typeof entry !== "string") {
|
|
1175
|
+
throw new Error(`${fieldName} must be an array of strings`);
|
|
1176
|
+
}
|
|
1177
|
+
const trimmed = entry.trim();
|
|
1178
|
+
if (trimmed) values.push(trimmed);
|
|
1179
|
+
}
|
|
1180
|
+
return values;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
function parseSetSessionModelConfig(msg) {
|
|
1184
|
+
if (!msg || typeof msg !== "object") {
|
|
1185
|
+
throw new Error("setSessionModelConfig payload must be an object");
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
const payload = {};
|
|
1189
|
+
|
|
1190
|
+
const modelRef = parseOptionalTrimmedString(msg.modelRef);
|
|
1191
|
+
if (modelRef) {
|
|
1192
|
+
if (!modelRef.includes("/")) {
|
|
1193
|
+
throw new Error("modelRef must be in provider/id format");
|
|
1194
|
+
}
|
|
1195
|
+
payload.modelRef = modelRef;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
if (Object.prototype.hasOwnProperty.call(msg, "thinkingLevel")) {
|
|
1199
|
+
if (typeof msg.thinkingLevel !== "string") {
|
|
1200
|
+
throw new Error(
|
|
1201
|
+
"thinkingLevel must be blank|off|minimal|low|medium|high|xhigh",
|
|
1202
|
+
);
|
|
1203
|
+
}
|
|
1204
|
+
const normalized = msg.thinkingLevel.trim().toLowerCase();
|
|
1205
|
+
if (
|
|
1206
|
+
normalized &&
|
|
1207
|
+
!["off", "minimal", "low", "medium", "high", "xhigh"].includes(normalized)
|
|
1208
|
+
) {
|
|
1209
|
+
throw new Error(
|
|
1210
|
+
"thinkingLevel must be blank|off|minimal|low|medium|high|xhigh",
|
|
1211
|
+
);
|
|
1212
|
+
}
|
|
1213
|
+
payload.thinkingLevel = normalized;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
const reasoningEnabled = parseOptionalBoolean(
|
|
1217
|
+
msg.reasoningEnabled,
|
|
1218
|
+
"reasoningEnabled",
|
|
1219
|
+
);
|
|
1220
|
+
if (reasoningEnabled !== undefined) {
|
|
1221
|
+
payload.reasoningEnabled = reasoningEnabled;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
const verboseLevel = parseOptionalTrimmedString(msg.verboseLevel);
|
|
1225
|
+
if (verboseLevel) {
|
|
1226
|
+
const normalized = verboseLevel.toLowerCase();
|
|
1227
|
+
if (!["off", "on", "full"].includes(normalized)) {
|
|
1228
|
+
throw new Error("verboseLevel must be off|on|full");
|
|
1229
|
+
}
|
|
1230
|
+
payload.verboseLevel = normalized;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
if (Object.keys(payload).length === 0) {
|
|
1234
|
+
throw new Error("setSessionModelConfig requires at least one field");
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
return payload;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
function parseSetEvenAiSettings(msg) {
|
|
1241
|
+
if (!msg || typeof msg !== "object") {
|
|
1242
|
+
throw new Error("setEvenAiSettings payload must be an object");
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
const payload = {};
|
|
1246
|
+
|
|
1247
|
+
if (Object.prototype.hasOwnProperty.call(msg, "routingMode")) {
|
|
1248
|
+
const routingMode = parseOptionalTrimmedString(msg.routingMode);
|
|
1249
|
+
if (!routingMode) {
|
|
1250
|
+
throw new Error("routingMode must be active|background|background_new");
|
|
1251
|
+
}
|
|
1252
|
+
const normalizedInput = routingMode.toLowerCase();
|
|
1253
|
+
if (
|
|
1254
|
+
![
|
|
1255
|
+
"active",
|
|
1256
|
+
"background",
|
|
1257
|
+
"background_new",
|
|
1258
|
+
"dedicated",
|
|
1259
|
+
"new",
|
|
1260
|
+
"dedicated_shadow",
|
|
1261
|
+
"new_shadow",
|
|
1262
|
+
].includes(normalizedInput)
|
|
1263
|
+
) {
|
|
1264
|
+
throw new Error("routingMode must be active|background|background_new");
|
|
1265
|
+
}
|
|
1266
|
+
payload.routingMode = normalizeEvenAiRoutingMode(routingMode);
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
if (Object.prototype.hasOwnProperty.call(msg, "systemPrompt")) {
|
|
1270
|
+
if (typeof msg.systemPrompt !== "string") {
|
|
1271
|
+
throw new Error("systemPrompt must be a string");
|
|
1272
|
+
}
|
|
1273
|
+
payload.systemPrompt = msg.systemPrompt.trim();
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
if (Object.prototype.hasOwnProperty.call(msg, "defaultModel")) {
|
|
1277
|
+
if (typeof msg.defaultModel !== "string") {
|
|
1278
|
+
throw new Error("defaultModel must be a string");
|
|
1279
|
+
}
|
|
1280
|
+
payload.defaultModel = msg.defaultModel.trim();
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
if (Object.prototype.hasOwnProperty.call(msg, "defaultThinking")) {
|
|
1284
|
+
if (typeof msg.defaultThinking !== "string") {
|
|
1285
|
+
throw new Error("defaultThinking must be a string");
|
|
1286
|
+
}
|
|
1287
|
+
const normalizedThinking = msg.defaultThinking.trim().toLowerCase();
|
|
1288
|
+
if (
|
|
1289
|
+
normalizedThinking &&
|
|
1290
|
+
!["off", "minimal", "low", "medium", "high", "xhigh"].includes(normalizedThinking)
|
|
1291
|
+
) {
|
|
1292
|
+
throw new Error("defaultThinking must be off|minimal|low|medium|high|xhigh");
|
|
1293
|
+
}
|
|
1294
|
+
payload.defaultThinking = normalizedThinking;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
if (Object.prototype.hasOwnProperty.call(msg, "listenEnabled")) {
|
|
1298
|
+
if (typeof msg.listenEnabled !== "boolean") {
|
|
1299
|
+
throw new Error("listenEnabled must be a boolean");
|
|
1300
|
+
}
|
|
1301
|
+
payload.listenEnabled = msg.listenEnabled;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
if (Object.keys(payload).length === 0) {
|
|
1305
|
+
throw new Error("setEvenAiSettings requires at least one field");
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
return payload;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
function parseSetOcuClawSettings(msg) {
|
|
1312
|
+
if (!msg || typeof msg !== "object") {
|
|
1313
|
+
throw new Error("setOcuClawSettings payload must be an object");
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
const payload = {};
|
|
1317
|
+
|
|
1318
|
+
if (Object.prototype.hasOwnProperty.call(msg, "systemPrompt")) {
|
|
1319
|
+
if (typeof msg.systemPrompt !== "string") {
|
|
1320
|
+
throw new Error("systemPrompt must be a string");
|
|
1321
|
+
}
|
|
1322
|
+
payload.systemPrompt = msg.systemPrompt.trim();
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
if (Object.prototype.hasOwnProperty.call(msg, "defaultModel")) {
|
|
1326
|
+
if (typeof msg.defaultModel !== "string") {
|
|
1327
|
+
throw new Error("defaultModel must be a string");
|
|
1328
|
+
}
|
|
1329
|
+
payload.defaultModel = normalizeOcuClawDefaultModel(msg.defaultModel);
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
if (Object.prototype.hasOwnProperty.call(msg, "defaultThinking")) {
|
|
1333
|
+
if (typeof msg.defaultThinking !== "string") {
|
|
1334
|
+
throw new Error("defaultThinking must be a string");
|
|
1335
|
+
}
|
|
1336
|
+
payload.defaultThinking = normalizeOcuClawDefaultThinking(msg.defaultThinking);
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
if (Object.keys(payload).length === 0) {
|
|
1340
|
+
throw new Error("setOcuClawSettings requires at least one field");
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
return payload;
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
function parseRequestSonioxTemporaryKey(msg) {
|
|
1347
|
+
if (!msg || typeof msg !== "object") {
|
|
1348
|
+
throw new Error("requestSonioxTemporaryKey payload must be an object");
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
const voiceSessionId = parseOptionalTrimmedString(msg.voiceSessionId);
|
|
1352
|
+
if (!voiceSessionId) {
|
|
1353
|
+
throw new Error("voiceSessionId is required");
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
return {
|
|
1357
|
+
voiceSessionId,
|
|
1358
|
+
sessionKey: parseOptionalTrimmedString(msg.sessionKey),
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
function parseApprovalResponsePayload(msg) {
|
|
1363
|
+
if (!msg || typeof msg !== "object") {
|
|
1364
|
+
throw new Error("ocuclaw.approval.resolve payload must be an object");
|
|
1365
|
+
}
|
|
1366
|
+
const id = parseOptionalTrimmedString(msg.id);
|
|
1367
|
+
if (!id) {
|
|
1368
|
+
throw new Error("ocuclaw.approval.resolve id is required");
|
|
1369
|
+
}
|
|
1370
|
+
const decisionRaw = parseOptionalTrimmedString(msg.decision);
|
|
1371
|
+
if (!decisionRaw) {
|
|
1372
|
+
throw new Error("ocuclaw.approval.resolve decision is required");
|
|
1373
|
+
}
|
|
1374
|
+
const decision = decisionRaw.toLowerCase();
|
|
1375
|
+
if (!APPROVAL_DECISIONS.has(decision)) {
|
|
1376
|
+
throw new Error(
|
|
1377
|
+
"ocuclaw.approval.resolve decision must be allow-once|allow-always|deny",
|
|
1378
|
+
);
|
|
1379
|
+
}
|
|
1380
|
+
return {
|
|
1381
|
+
id,
|
|
1382
|
+
decision,
|
|
1383
|
+
requestId: parseOptionalTrimmedString(msg.requestId) || null,
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
function pruneApprovalResolveCache(nowMs) {
|
|
1388
|
+
for (const [key, entry] of approvalResolveCache) {
|
|
1389
|
+
if (!entry || entry.expiresAtMs <= nowMs) {
|
|
1390
|
+
approvalResolveCache.delete(key);
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
while (approvalResolveCache.size > approvalResolveCacheMaxEntries) {
|
|
1394
|
+
const oldest = approvalResolveCache.keys().next();
|
|
1395
|
+
if (oldest.done) break;
|
|
1396
|
+
approvalResolveCache.delete(oldest.value);
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
function parseRemoteControl(msg) {
|
|
1401
|
+
if (!msg || typeof msg !== "object") {
|
|
1402
|
+
throw new Error("remote-control payload must be an object");
|
|
1403
|
+
}
|
|
1404
|
+
const actionRaw = parseOptionalTrimmedString(msg.action);
|
|
1405
|
+
if (!actionRaw) {
|
|
1406
|
+
throw new Error("remote-control action is required");
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
const action = actionRaw.toLowerCase();
|
|
1410
|
+
const payload = {
|
|
1411
|
+
action,
|
|
1412
|
+
requestId: parseOptionalTrimmedString(msg.requestId) || null,
|
|
1413
|
+
};
|
|
1414
|
+
|
|
1415
|
+
if (action === "button") {
|
|
1416
|
+
payload.button = normalizeRemoteButton(msg.button);
|
|
1417
|
+
return payload;
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
if (action === "send-message") {
|
|
1421
|
+
const text = typeof msg.text === "string" ? msg.text : "";
|
|
1422
|
+
if (!text.trim()) {
|
|
1423
|
+
throw new Error("remote-control send-message requires non-empty text");
|
|
1424
|
+
}
|
|
1425
|
+
payload.text = text;
|
|
1426
|
+
const sessionKey = parseOptionalTrimmedString(msg.sessionKey);
|
|
1427
|
+
if (sessionKey) payload.sessionKey = sessionKey;
|
|
1428
|
+
return payload;
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
if (action === "relay-action") {
|
|
1432
|
+
payload.relayAction = normalizeRemoteRelayAction(msg.relayAction);
|
|
1433
|
+
if (
|
|
1434
|
+
payload.relayAction === "listen-start" ||
|
|
1435
|
+
payload.relayAction === "listen-stop" ||
|
|
1436
|
+
payload.relayAction === "listen-send" ||
|
|
1437
|
+
payload.relayAction === "listen-retry"
|
|
1438
|
+
) {
|
|
1439
|
+
throw new Error(
|
|
1440
|
+
`remote-control relayAction ${payload.relayAction} was removed; voice stays local to the app`,
|
|
1441
|
+
);
|
|
1442
|
+
}
|
|
1443
|
+
const sessionKey = parseOptionalTrimmedString(msg.sessionKey);
|
|
1444
|
+
const command = parseOptionalTrimmedString(msg.command);
|
|
1445
|
+
if (payload.relayAction === "switch-session" && !sessionKey) {
|
|
1446
|
+
throw new Error("remote-control switch-session requires sessionKey");
|
|
1447
|
+
}
|
|
1448
|
+
if (payload.relayAction === "slash-command" && !command) {
|
|
1449
|
+
throw new Error("remote-control slash-command requires command");
|
|
1450
|
+
}
|
|
1451
|
+
if (sessionKey) payload.sessionKey = sessionKey;
|
|
1452
|
+
if (command) payload.command = command;
|
|
1453
|
+
const endpointDetection = parseOptionalBoolean(msg.endpointDetection, "endpointDetection");
|
|
1454
|
+
if (endpointDetection !== undefined) payload.endpointDetection = endpointDetection;
|
|
1455
|
+
const maxEndpointDelayMs = parseOptionalPositiveNumber(
|
|
1456
|
+
msg.maxEndpointDelayMs,
|
|
1457
|
+
"maxEndpointDelayMs",
|
|
1458
|
+
);
|
|
1459
|
+
if (maxEndpointDelayMs !== undefined) payload.maxEndpointDelayMs = maxEndpointDelayMs;
|
|
1460
|
+
const model = parseOptionalTrimmedString(msg.model);
|
|
1461
|
+
if (model) payload.model = model;
|
|
1462
|
+
const languageHints = parseOptionalStringArray(msg.languageHints, "languageHints");
|
|
1463
|
+
if (languageHints !== undefined) payload.languageHints = languageHints;
|
|
1464
|
+
return payload;
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
if (action === "list-click") {
|
|
1468
|
+
const index = parseOptionalNonNegativeNumber(msg.index, "index");
|
|
1469
|
+
if (index === undefined) {
|
|
1470
|
+
throw new Error("remote-control list-click requires index");
|
|
1471
|
+
}
|
|
1472
|
+
payload.index = index;
|
|
1473
|
+
const containerId = parseOptionalNonNegativeNumber(msg.containerId, "containerId");
|
|
1474
|
+
const containerName = parseOptionalTrimmedString(msg.containerName);
|
|
1475
|
+
if (containerId !== undefined) payload.containerId = containerId;
|
|
1476
|
+
if (containerName) payload.containerName = containerName;
|
|
1477
|
+
return payload;
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
if (action === "text-event") {
|
|
1481
|
+
payload.eventType = normalizeRemoteButton(msg.eventType || msg.button);
|
|
1482
|
+
const containerId = parseOptionalNonNegativeNumber(msg.containerId, "containerId");
|
|
1483
|
+
const containerName = parseOptionalTrimmedString(msg.containerName);
|
|
1484
|
+
if (containerId !== undefined) payload.containerId = containerId;
|
|
1485
|
+
if (containerName) payload.containerName = containerName;
|
|
1486
|
+
return payload;
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
throw new Error(`unsupported remote-control action: ${actionRaw}`);
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
const ATTACHMENT_MAX_DECODED_BYTES = 5_000_000;
|
|
1493
|
+
const ATTACHMENT_MAX_ENCODED_CHARS =
|
|
1494
|
+
Math.ceil((ATTACHMENT_MAX_DECODED_BYTES * 4) / 3) + 16;
|
|
1495
|
+
|
|
1496
|
+
function stripDataUrlPrefix(value) {
|
|
1497
|
+
if (typeof value !== "string") return "";
|
|
1498
|
+
if (!value.startsWith("data:")) return value;
|
|
1499
|
+
const comma = value.indexOf(",");
|
|
1500
|
+
return comma >= 0 ? value.slice(comma + 1) : value;
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
function parseOptionalPositiveInt(value) {
|
|
1504
|
+
if (value === undefined || value === null) return null;
|
|
1505
|
+
const num = Number(value);
|
|
1506
|
+
if (!Number.isFinite(num) || num <= 0) return null;
|
|
1507
|
+
return Math.floor(num);
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
function rejectAttachment(errorCode, error) {
|
|
1511
|
+
return {
|
|
1512
|
+
ok: false,
|
|
1513
|
+
errorCode,
|
|
1514
|
+
error,
|
|
1515
|
+
};
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
function parseAttachment(rawAttachment) {
|
|
1519
|
+
if (rawAttachment === undefined || rawAttachment === null) {
|
|
1520
|
+
return { ok: true, attachment: null };
|
|
1521
|
+
}
|
|
1522
|
+
if (typeof rawAttachment !== "object" || Array.isArray(rawAttachment)) {
|
|
1523
|
+
return rejectAttachment(
|
|
1524
|
+
"attachment_invalid_type",
|
|
1525
|
+
"attachment must be an object",
|
|
1526
|
+
);
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
const kind = parseOptionalTrimmedString(rawAttachment.kind) || "image";
|
|
1530
|
+
if (kind !== "image") {
|
|
1531
|
+
return rejectAttachment(
|
|
1532
|
+
"attachment_invalid_type",
|
|
1533
|
+
"unsupported attachment kind",
|
|
1534
|
+
);
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
const mimeTypeRaw = parseOptionalTrimmedString(rawAttachment.mimeType);
|
|
1538
|
+
const mimeType = mimeTypeRaw ? mimeTypeRaw.toLowerCase() : null;
|
|
1539
|
+
if (!mimeType || !mimeType.startsWith("image/")) {
|
|
1540
|
+
return rejectAttachment(
|
|
1541
|
+
"attachment_invalid_type",
|
|
1542
|
+
"attachment mimeType must be image/*",
|
|
1543
|
+
);
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
const base64Raw = parseOptionalTrimmedString(rawAttachment.base64Data);
|
|
1547
|
+
if (!base64Raw) {
|
|
1548
|
+
return rejectAttachment(
|
|
1549
|
+
"attachment_missing_data",
|
|
1550
|
+
"attachment base64Data is required",
|
|
1551
|
+
);
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
const base64Data = stripDataUrlPrefix(base64Raw).replace(/\s+/g, "");
|
|
1555
|
+
if (!base64Data) {
|
|
1556
|
+
return rejectAttachment(
|
|
1557
|
+
"attachment_missing_data",
|
|
1558
|
+
"attachment base64Data is required",
|
|
1559
|
+
);
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
if (base64Data.length > ATTACHMENT_MAX_ENCODED_CHARS) {
|
|
1563
|
+
return rejectAttachment(
|
|
1564
|
+
"attachment_too_large_encoded",
|
|
1565
|
+
`attachment payload exceeds encoded limit (${ATTACHMENT_MAX_ENCODED_CHARS} chars)`,
|
|
1566
|
+
);
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
if (!/^[A-Za-z0-9+/]+={0,2}$/.test(base64Data)) {
|
|
1570
|
+
return rejectAttachment(
|
|
1571
|
+
"attachment_decode_failed",
|
|
1572
|
+
"attachment base64Data is not valid base64",
|
|
1573
|
+
);
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
let decoded;
|
|
1577
|
+
try {
|
|
1578
|
+
decoded = Buffer.from(base64Data, "base64");
|
|
1579
|
+
} catch {
|
|
1580
|
+
return rejectAttachment(
|
|
1581
|
+
"attachment_decode_failed",
|
|
1582
|
+
"attachment base64Data decode failed",
|
|
1583
|
+
);
|
|
1584
|
+
}
|
|
1585
|
+
if (!decoded || decoded.length <= 0) {
|
|
1586
|
+
return rejectAttachment(
|
|
1587
|
+
"attachment_missing_data",
|
|
1588
|
+
"attachment decoded payload is empty",
|
|
1589
|
+
);
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
const canonical = decoded.toString("base64").replace(/=+$/g, "");
|
|
1593
|
+
const providedCanonical = base64Data.replace(/=+$/g, "");
|
|
1594
|
+
if (canonical !== providedCanonical) {
|
|
1595
|
+
return rejectAttachment(
|
|
1596
|
+
"attachment_decode_failed",
|
|
1597
|
+
"attachment base64Data decode failed",
|
|
1598
|
+
);
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
if (decoded.length > ATTACHMENT_MAX_DECODED_BYTES) {
|
|
1602
|
+
return rejectAttachment(
|
|
1603
|
+
"attachment_too_large",
|
|
1604
|
+
`attachment exceeds ${ATTACHMENT_MAX_DECODED_BYTES} byte decoded limit`,
|
|
1605
|
+
);
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
const sourceRaw = parseOptionalTrimmedString(rawAttachment.source);
|
|
1609
|
+
const sourceNormalized = sourceRaw ? sourceRaw.toLowerCase() : null;
|
|
1610
|
+
const source =
|
|
1611
|
+
sourceNormalized === "camera" || sourceNormalized === "gallery"
|
|
1612
|
+
? sourceNormalized
|
|
1613
|
+
: null;
|
|
1614
|
+
|
|
1615
|
+
return {
|
|
1616
|
+
ok: true,
|
|
1617
|
+
attachment: {
|
|
1618
|
+
kind: "image",
|
|
1619
|
+
name: parseOptionalTrimmedString(rawAttachment.name) || "image.jpg",
|
|
1620
|
+
mimeType,
|
|
1621
|
+
base64Data,
|
|
1622
|
+
sizeBytes: decoded.length,
|
|
1623
|
+
widthPx: parseOptionalPositiveInt(rawAttachment.widthPx),
|
|
1624
|
+
heightPx: parseOptionalPositiveInt(rawAttachment.heightPx),
|
|
1625
|
+
source,
|
|
1626
|
+
},
|
|
1627
|
+
};
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
// --- Message handlers ---
|
|
1631
|
+
|
|
1632
|
+
/**
|
|
1633
|
+
* Handle a "send" message: forward user text to upstream agent.
|
|
1634
|
+
*
|
|
1635
|
+
* @param {string} clientId
|
|
1636
|
+
* @param {object} msg - Parsed message with id, text, sessionKey, and optional attachment
|
|
1637
|
+
* @returns {{ unicast: string }|Promise<{ unicast: string }>}
|
|
1638
|
+
*/
|
|
1639
|
+
function handleSend(clientId, msg) {
|
|
1640
|
+
const requestId = parseOptionalTrimmedString(msg.requestId);
|
|
1641
|
+
if (!requestId) {
|
|
1642
|
+
return {
|
|
1643
|
+
unicast: formatSendAck(
|
|
1644
|
+
requestId,
|
|
1645
|
+
"rejected",
|
|
1646
|
+
"Missing required field: requestId",
|
|
1647
|
+
),
|
|
1648
|
+
};
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
const parsedAttachment = parseAttachment(msg.attachment);
|
|
1652
|
+
if (!parsedAttachment.ok) {
|
|
1653
|
+
return {
|
|
1654
|
+
unicast: formatSendAck(
|
|
1655
|
+
requestId,
|
|
1656
|
+
"rejected",
|
|
1657
|
+
parsedAttachment.error,
|
|
1658
|
+
parsedAttachment.errorCode,
|
|
1659
|
+
),
|
|
1660
|
+
};
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
const text = typeof msg.text === "string" ? msg.text : "";
|
|
1664
|
+
if (!text.trim() && !parsedAttachment.attachment) {
|
|
1665
|
+
return {
|
|
1666
|
+
unicast: formatSendAck(
|
|
1667
|
+
requestId,
|
|
1668
|
+
"rejected",
|
|
1669
|
+
"Missing required field: text",
|
|
1670
|
+
),
|
|
1671
|
+
};
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
// Check upstream connectivity
|
|
1675
|
+
if (!isUpstreamConnected()) {
|
|
1676
|
+
return {
|
|
1677
|
+
unicast: formatSendAck(
|
|
1678
|
+
requestId,
|
|
1679
|
+
"rejected",
|
|
1680
|
+
"OpenClaw disconnected",
|
|
1681
|
+
),
|
|
1682
|
+
};
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
// Forward to upstream — resolves on initial ack (accepted/queued)
|
|
1686
|
+
return onSend(
|
|
1687
|
+
requestId,
|
|
1688
|
+
text,
|
|
1689
|
+
msg.sessionKey || null,
|
|
1690
|
+
parsedAttachment.attachment,
|
|
1691
|
+
).then(
|
|
1692
|
+
(result) => {
|
|
1693
|
+
const status = (result && result.status) || "accepted";
|
|
1694
|
+
return { unicast: formatSendAck(requestId, status) };
|
|
1695
|
+
},
|
|
1696
|
+
(err) => ({
|
|
1697
|
+
unicast: formatSendAck(
|
|
1698
|
+
requestId,
|
|
1699
|
+
"rejected",
|
|
1700
|
+
err.message || "Send failed",
|
|
1701
|
+
err.errorCode || undefined,
|
|
1702
|
+
),
|
|
1703
|
+
}),
|
|
1704
|
+
);
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
/**
|
|
1708
|
+
* Handle a "simulate" message: inject fake message into conversation state.
|
|
1709
|
+
*
|
|
1710
|
+
* @param {string} clientId
|
|
1711
|
+
* @param {object} msg - Parsed message with sender, text
|
|
1712
|
+
* @returns {{ broadcast: string }}
|
|
1713
|
+
*/
|
|
1714
|
+
function handleSimulate(clientId, msg) {
|
|
1715
|
+
const pages = onSimulate(
|
|
1716
|
+
msg.sender || "Simulator",
|
|
1717
|
+
msg.text || "",
|
|
1718
|
+
);
|
|
1719
|
+
return { broadcast: formatPages(pages) };
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
/**
|
|
1723
|
+
* Handle a "simulateStream" message: relay-local deterministic stream replay.
|
|
1724
|
+
*
|
|
1725
|
+
* @param {string} clientId
|
|
1726
|
+
* @param {object} msg - Parsed message with id, text, sender, and timing controls.
|
|
1727
|
+
* @returns {{ unicast: string }|Promise<{ unicast: string }>}
|
|
1728
|
+
*/
|
|
1729
|
+
function handleSimulateStream(clientId, msg) {
|
|
1730
|
+
const id = parseOptionalTrimmedString(msg.id);
|
|
1731
|
+
if (!id) {
|
|
1732
|
+
return {
|
|
1733
|
+
unicast: formatSendAck(
|
|
1734
|
+
msg.id || null,
|
|
1735
|
+
"rejected",
|
|
1736
|
+
"Missing required field: id",
|
|
1737
|
+
),
|
|
1738
|
+
};
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
if (!onSimulateStream) {
|
|
1742
|
+
return {
|
|
1743
|
+
unicast: formatSendAck(
|
|
1744
|
+
id,
|
|
1745
|
+
"rejected",
|
|
1746
|
+
"simulateStream not supported by relay",
|
|
1747
|
+
),
|
|
1748
|
+
};
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
const text = typeof msg.text === "string" ? msg.text : "";
|
|
1752
|
+
if (!text.trim()) {
|
|
1753
|
+
return {
|
|
1754
|
+
unicast: formatSendAck(
|
|
1755
|
+
id,
|
|
1756
|
+
"rejected",
|
|
1757
|
+
"simulateStream requires non-empty text",
|
|
1758
|
+
),
|
|
1759
|
+
};
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
let chunkChars;
|
|
1763
|
+
let chunkIntervalMs;
|
|
1764
|
+
let startDelayMs;
|
|
1765
|
+
let thinkingTailMs;
|
|
1766
|
+
try {
|
|
1767
|
+
chunkChars = parseOptionalPositiveNumber(msg.chunkChars, "chunkChars");
|
|
1768
|
+
chunkIntervalMs = parseOptionalPositiveNumber(msg.chunkIntervalMs, "chunkIntervalMs");
|
|
1769
|
+
startDelayMs = parseOptionalNonNegativeNumber(msg.startDelayMs, "startDelayMs");
|
|
1770
|
+
thinkingTailMs = parseOptionalNonNegativeNumber(msg.thinkingTailMs, "thinkingTailMs");
|
|
1771
|
+
} catch (err) {
|
|
1772
|
+
return {
|
|
1773
|
+
unicast: formatSendAck(
|
|
1774
|
+
id,
|
|
1775
|
+
"rejected",
|
|
1776
|
+
err && err.message ? err.message : "Invalid simulateStream parameters",
|
|
1777
|
+
),
|
|
1778
|
+
};
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
const request = {
|
|
1782
|
+
id,
|
|
1783
|
+
sender: parseOptionalTrimmedString(msg.sender) || "Simulator",
|
|
1784
|
+
text,
|
|
1785
|
+
sessionKey: parseOptionalTrimmedString(msg.sessionKey) || null,
|
|
1786
|
+
chunkChars,
|
|
1787
|
+
chunkIntervalMs,
|
|
1788
|
+
startDelayMs,
|
|
1789
|
+
thinkingTailMs,
|
|
1790
|
+
};
|
|
1791
|
+
|
|
1792
|
+
return Promise.resolve(onSimulateStream(request)).then(
|
|
1793
|
+
(result) => {
|
|
1794
|
+
const status = result && result.status ? result.status : "accepted";
|
|
1795
|
+
const error = result && result.error ? result.error : undefined;
|
|
1796
|
+
return { unicast: formatSendAck(id, status, error) };
|
|
1797
|
+
},
|
|
1798
|
+
(err) => ({
|
|
1799
|
+
unicast: formatSendAck(
|
|
1800
|
+
id,
|
|
1801
|
+
"rejected",
|
|
1802
|
+
err && err.message ? err.message : "simulateStream failed",
|
|
1803
|
+
),
|
|
1804
|
+
}),
|
|
1805
|
+
);
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
/**
|
|
1809
|
+
* Handle a "subscribeProtocol" message: mark client for protocol forwarding.
|
|
1810
|
+
*
|
|
1811
|
+
* @param {string} clientId
|
|
1812
|
+
* @returns {null}
|
|
1813
|
+
*/
|
|
1814
|
+
function handleSubscribeProtocol(clientId) {
|
|
1815
|
+
protocolSubscribers.add(clientId);
|
|
1816
|
+
return null;
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
/**
|
|
1820
|
+
* Handle an "approvalResponse" message: forward decision to upstream gateway.
|
|
1821
|
+
*
|
|
1822
|
+
* @param {string} clientId
|
|
1823
|
+
* @param {object} msg - Parsed message with id and decision
|
|
1824
|
+
* @returns {{ unicast: string }|Promise<{ unicast: string }>}
|
|
1825
|
+
*/
|
|
1826
|
+
function handleApprovalResponse(clientId, msg) {
|
|
1827
|
+
let payload;
|
|
1828
|
+
try {
|
|
1829
|
+
payload = parseApprovalResponsePayload(msg);
|
|
1830
|
+
} catch (err) {
|
|
1831
|
+
return {
|
|
1832
|
+
unicast: formatApprovalResponseAck({
|
|
1833
|
+
id: parseOptionalTrimmedString(msg && msg.id) || null,
|
|
1834
|
+
decision: parseOptionalTrimmedString(msg && msg.decision) || null,
|
|
1835
|
+
requestId: parseOptionalTrimmedString(msg && msg.requestId) || null,
|
|
1836
|
+
status: "rejected",
|
|
1837
|
+
code: "invalid_approval_response",
|
|
1838
|
+
message:
|
|
1839
|
+
err && err.message
|
|
1840
|
+
? err.message
|
|
1841
|
+
: "Invalid ocuclaw.approval.resolve payload",
|
|
1842
|
+
idempotent: false,
|
|
1843
|
+
}),
|
|
1844
|
+
};
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
if (!onApprovalResolve) {
|
|
1848
|
+
return {
|
|
1849
|
+
unicast: formatApprovalResponseAck({
|
|
1850
|
+
id: payload.id,
|
|
1851
|
+
decision: payload.decision,
|
|
1852
|
+
requestId: payload.requestId,
|
|
1853
|
+
status: "rejected",
|
|
1854
|
+
code: "approval_unavailable",
|
|
1855
|
+
message: "approval resolution is not available",
|
|
1856
|
+
idempotent: false,
|
|
1857
|
+
}),
|
|
1858
|
+
};
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
const nowMs = Date.now();
|
|
1862
|
+
pruneApprovalResolveCache(nowMs);
|
|
1863
|
+
const idempotencyScope = payload.requestId || `client:${clientId}`;
|
|
1864
|
+
const cacheKey = `${payload.id}|${payload.decision}|${idempotencyScope}`;
|
|
1865
|
+
const existing = approvalResolveCache.get(cacheKey);
|
|
1866
|
+
if (existing && existing.expiresAtMs > nowMs) {
|
|
1867
|
+
existing.expiresAtMs = nowMs + approvalResolveCacheTtlMs;
|
|
1868
|
+
return existing.promise.then((ack) => ({
|
|
1869
|
+
unicast: formatApprovalResponseAck({
|
|
1870
|
+
...ack,
|
|
1871
|
+
idempotent: true,
|
|
1872
|
+
code:
|
|
1873
|
+
ack && ack.status === "accepted"
|
|
1874
|
+
? "duplicate_request"
|
|
1875
|
+
: ack && ack.code
|
|
1876
|
+
? ack.code
|
|
1877
|
+
: "duplicate_request",
|
|
1878
|
+
}),
|
|
1879
|
+
}));
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
const promise = Promise.resolve(
|
|
1883
|
+
onApprovalResolve(
|
|
1884
|
+
payload.id,
|
|
1885
|
+
payload.decision,
|
|
1886
|
+
{ requestId: payload.requestId, clientId },
|
|
1887
|
+
),
|
|
1888
|
+
).then(
|
|
1889
|
+
() => ({
|
|
1890
|
+
id: payload.id,
|
|
1891
|
+
decision: payload.decision,
|
|
1892
|
+
requestId: payload.requestId,
|
|
1893
|
+
status: "accepted",
|
|
1894
|
+
code: "ok",
|
|
1895
|
+
message: null,
|
|
1896
|
+
idempotent: false,
|
|
1897
|
+
}),
|
|
1898
|
+
(err) => {
|
|
1899
|
+
const message =
|
|
1900
|
+
err && err.message ? err.message : "approvalResolve failed";
|
|
1901
|
+
logger.error(`[downstream] approvalResolve failed: ${message}`);
|
|
1902
|
+
return {
|
|
1903
|
+
id: payload.id,
|
|
1904
|
+
decision: payload.decision,
|
|
1905
|
+
requestId: payload.requestId,
|
|
1906
|
+
status: "rejected",
|
|
1907
|
+
code: "approval_resolve_failed",
|
|
1908
|
+
message,
|
|
1909
|
+
idempotent: false,
|
|
1910
|
+
};
|
|
1911
|
+
},
|
|
1912
|
+
);
|
|
1913
|
+
|
|
1914
|
+
const cacheEntry = {
|
|
1915
|
+
expiresAtMs: nowMs + approvalResolveCacheTtlMs,
|
|
1916
|
+
promise,
|
|
1917
|
+
};
|
|
1918
|
+
approvalResolveCache.set(cacheKey, cacheEntry);
|
|
1919
|
+
pruneApprovalResolveCache(nowMs);
|
|
1920
|
+
|
|
1921
|
+
return promise.then((ack) => {
|
|
1922
|
+
if (ack && ack.status === "accepted") {
|
|
1923
|
+
cacheEntry.expiresAtMs = Date.now() + approvalResolveCacheTtlMs;
|
|
1924
|
+
} else {
|
|
1925
|
+
approvalResolveCache.delete(cacheKey);
|
|
1926
|
+
}
|
|
1927
|
+
return { unicast: formatApprovalResponseAck(ack) };
|
|
1928
|
+
});
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
/**
|
|
1932
|
+
* Handle a "newChat" message: clear conversation and reset the session.
|
|
1933
|
+
*
|
|
1934
|
+
* @param {string} clientId
|
|
1935
|
+
* @returns {Promise<{ broadcast: string }>}
|
|
1936
|
+
*/
|
|
1937
|
+
function handleNewChat(clientId) {
|
|
1938
|
+
return onNewChat().then(
|
|
1939
|
+
(pages) => ({ broadcast: formatPages(pages) }),
|
|
1940
|
+
(err) => {
|
|
1941
|
+
logger.error(`[downstream] newChat failed: ${err.message}`);
|
|
1942
|
+
return null;
|
|
1943
|
+
},
|
|
1944
|
+
);
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
/**
|
|
1948
|
+
* Handle a "getSessions" message: fetch session list from upstream.
|
|
1949
|
+
*
|
|
1950
|
+
* @param {string} clientId
|
|
1951
|
+
* @returns {Promise<{ unicast: string }>}
|
|
1952
|
+
*/
|
|
1953
|
+
function handleGetSessions(clientId) {
|
|
1954
|
+
return onGetSessions().then(
|
|
1955
|
+
(sessions) => ({ unicast: formatSessions(sessions) }),
|
|
1956
|
+
(err) => {
|
|
1957
|
+
logger.error(`[downstream] getSessions failed: ${err.message}`);
|
|
1958
|
+
return { unicast: formatSessions([]) };
|
|
1959
|
+
},
|
|
1960
|
+
);
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
/**
|
|
1964
|
+
* Handle "getModelsCatalog": return cached model catalog snapshot.
|
|
1965
|
+
*
|
|
1966
|
+
* @param {string} clientId
|
|
1967
|
+
* @returns {Promise<{ unicast: string }>|{ unicast: string }}
|
|
1968
|
+
*/
|
|
1969
|
+
function handleGetModelsCatalog(clientId) {
|
|
1970
|
+
if (!onGetModelsCatalog) {
|
|
1971
|
+
return {
|
|
1972
|
+
unicast: formatModelsCatalog({
|
|
1973
|
+
models: [],
|
|
1974
|
+
fetchedAtMs: Date.now(),
|
|
1975
|
+
stale: true,
|
|
1976
|
+
}),
|
|
1977
|
+
};
|
|
1978
|
+
}
|
|
1979
|
+
return Promise.resolve(onGetModelsCatalog()).then(
|
|
1980
|
+
(payload) => ({
|
|
1981
|
+
unicast: formatModelsCatalog(payload || {}),
|
|
1982
|
+
}),
|
|
1983
|
+
(err) => {
|
|
1984
|
+
logger.error(`[downstream] getModelsCatalog failed: ${err.message}`);
|
|
1985
|
+
return {
|
|
1986
|
+
unicast: formatModelsCatalog({
|
|
1987
|
+
models: [],
|
|
1988
|
+
fetchedAtMs: Date.now(),
|
|
1989
|
+
stale: true,
|
|
1990
|
+
}),
|
|
1991
|
+
};
|
|
1992
|
+
},
|
|
1993
|
+
);
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
/**
|
|
1997
|
+
* Handle "getSkills": return cached skills catalog snapshot.
|
|
1998
|
+
*
|
|
1999
|
+
* @param {string} clientId
|
|
2000
|
+
* @returns {Promise<{ unicast: string }>|{ unicast: string }}
|
|
2001
|
+
*/
|
|
2002
|
+
function handleGetSkillsCatalog(clientId) {
|
|
2003
|
+
if (!onGetSkillsCatalog) {
|
|
2004
|
+
return {
|
|
2005
|
+
unicast: formatSkillsCatalog({
|
|
2006
|
+
skills: [],
|
|
2007
|
+
fetchedAtMs: Date.now(),
|
|
2008
|
+
stale: true,
|
|
2009
|
+
}),
|
|
2010
|
+
};
|
|
2011
|
+
}
|
|
2012
|
+
return Promise.resolve(onGetSkillsCatalog()).then(
|
|
2013
|
+
(payload) => ({
|
|
2014
|
+
unicast: formatSkillsCatalog(payload || {}),
|
|
2015
|
+
}),
|
|
2016
|
+
(err) => {
|
|
2017
|
+
logger.error(`[downstream] getSkills failed: ${err.message}`);
|
|
2018
|
+
return {
|
|
2019
|
+
unicast: formatSkillsCatalog({
|
|
2020
|
+
skills: [],
|
|
2021
|
+
fetchedAtMs: Date.now(),
|
|
2022
|
+
stale: true,
|
|
2023
|
+
}),
|
|
2024
|
+
};
|
|
2025
|
+
},
|
|
2026
|
+
);
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
/**
|
|
2030
|
+
* Handle "getSonioxModels": return cached Soniox model snapshot.
|
|
2031
|
+
*
|
|
2032
|
+
* @param {string} clientId
|
|
2033
|
+
* @returns {Promise<{ unicast: string }>|{ unicast: string }}
|
|
2034
|
+
*/
|
|
2035
|
+
function handleGetSonioxModels(clientId) {
|
|
2036
|
+
if (!onGetSonioxModels) {
|
|
2037
|
+
return {
|
|
2038
|
+
unicast: formatSonioxModels({
|
|
2039
|
+
models: [],
|
|
2040
|
+
fetchedAtMs: Date.now(),
|
|
2041
|
+
stale: true,
|
|
2042
|
+
}),
|
|
2043
|
+
};
|
|
2044
|
+
}
|
|
2045
|
+
return Promise.resolve(onGetSonioxModels()).then(
|
|
2046
|
+
(payload) => ({
|
|
2047
|
+
unicast: formatSonioxModels(payload || {}),
|
|
2048
|
+
}),
|
|
2049
|
+
(err) => {
|
|
2050
|
+
logger.error(`[downstream] getSonioxModels failed: ${err.message}`);
|
|
2051
|
+
return {
|
|
2052
|
+
unicast: formatSonioxModels({
|
|
2053
|
+
models: [],
|
|
2054
|
+
fetchedAtMs: Date.now(),
|
|
2055
|
+
stale: true,
|
|
2056
|
+
}),
|
|
2057
|
+
};
|
|
2058
|
+
},
|
|
2059
|
+
);
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
/**
|
|
2063
|
+
* Handle "getStatus": return the current status snapshot.
|
|
2064
|
+
*
|
|
2065
|
+
* @param {string} clientId
|
|
2066
|
+
* @returns {{ unicast: string }}
|
|
2067
|
+
*/
|
|
2068
|
+
function handleGetStatus(clientId) {
|
|
2069
|
+
if (!onGetStatus) {
|
|
2070
|
+
return { unicast: formatError("getStatus is not available") };
|
|
2071
|
+
}
|
|
2072
|
+
try {
|
|
2073
|
+
return {
|
|
2074
|
+
unicast: formatStatus(onGetStatus() || {}),
|
|
2075
|
+
};
|
|
2076
|
+
} catch (err) {
|
|
2077
|
+
return { unicast: formatError(err.message || "getStatus failed") };
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
/**
|
|
2082
|
+
* Handle "getSessionModelConfig": read controls for current session key.
|
|
2083
|
+
*
|
|
2084
|
+
* @param {string} clientId
|
|
2085
|
+
* @returns {Promise<{ unicast: string }>|{ unicast: string }}
|
|
2086
|
+
*/
|
|
2087
|
+
function handleGetSessionModelConfig(clientId) {
|
|
2088
|
+
if (!onGetSessionModelConfig) {
|
|
2089
|
+
return { unicast: formatError("getSessionModelConfig is not available") };
|
|
2090
|
+
}
|
|
2091
|
+
return Promise.resolve(onGetSessionModelConfig()).then(
|
|
2092
|
+
(payload) => ({
|
|
2093
|
+
unicast: formatSessionModelConfig(payload || {}),
|
|
2094
|
+
}),
|
|
2095
|
+
(err) => {
|
|
2096
|
+
logger.error(`[downstream] getSessionModelConfig failed: ${err.message}`);
|
|
2097
|
+
return { unicast: formatError(err.message || "getSessionModelConfig failed") };
|
|
2098
|
+
},
|
|
2099
|
+
);
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
/**
|
|
2103
|
+
* Handle "setSessionModelConfig": patch controls for current session key.
|
|
2104
|
+
*
|
|
2105
|
+
* @param {string} clientId
|
|
2106
|
+
* @param {object} msg
|
|
2107
|
+
* @returns {Promise<{ unicast: string }>|{ unicast: string }}
|
|
2108
|
+
*/
|
|
2109
|
+
function handleSetSessionModelConfig(clientId, msg) {
|
|
2110
|
+
if (!onSetSessionModelConfig) {
|
|
2111
|
+
return {
|
|
2112
|
+
unicast: formatSessionModelConfigAck({
|
|
2113
|
+
status: "rejected",
|
|
2114
|
+
error: "setSessionModelConfig is not available",
|
|
2115
|
+
}),
|
|
2116
|
+
};
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
let payload;
|
|
2120
|
+
try {
|
|
2121
|
+
payload = parseSetSessionModelConfig(msg);
|
|
2122
|
+
} catch (err) {
|
|
2123
|
+
return {
|
|
2124
|
+
unicast: formatSessionModelConfigAck({
|
|
2125
|
+
status: "rejected",
|
|
2126
|
+
error: err && err.message ? err.message : "invalid setSessionModelConfig payload",
|
|
2127
|
+
}),
|
|
2128
|
+
};
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
return Promise.resolve(onSetSessionModelConfig(payload)).then(
|
|
2132
|
+
(result) =>
|
|
2133
|
+
({
|
|
2134
|
+
unicast: formatSessionModelConfigAck(result || { status: "accepted" }),
|
|
2135
|
+
}),
|
|
2136
|
+
(err) =>
|
|
2137
|
+
({
|
|
2138
|
+
unicast: formatSessionModelConfigAck({
|
|
2139
|
+
status: "rejected",
|
|
2140
|
+
error: err && err.message ? err.message : "setSessionModelConfig failed",
|
|
2141
|
+
}),
|
|
2142
|
+
}),
|
|
2143
|
+
);
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
/**
|
|
2147
|
+
* Handle "getEvenAiSettings": read relay-owned Even AI settings.
|
|
2148
|
+
*
|
|
2149
|
+
* @param {string} clientId
|
|
2150
|
+
* @returns {Promise<{ unicast: string }>|{ unicast: string }}
|
|
2151
|
+
*/
|
|
2152
|
+
function handleGetEvenAiSettings(clientId) {
|
|
2153
|
+
if (!onGetEvenAiSettings) {
|
|
2154
|
+
return { unicast: formatError("getEvenAiSettings is not available") };
|
|
2155
|
+
}
|
|
2156
|
+
return Promise.resolve(onGetEvenAiSettings()).then(
|
|
2157
|
+
(payload) => ({
|
|
2158
|
+
unicast: formatEvenAiSettings(payload || {}),
|
|
2159
|
+
}),
|
|
2160
|
+
(err) => {
|
|
2161
|
+
logger.error(`[downstream] getEvenAiSettings failed: ${err.message}`);
|
|
2162
|
+
return { unicast: formatError(err.message || "getEvenAiSettings failed") };
|
|
2163
|
+
},
|
|
2164
|
+
);
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
/**
|
|
2168
|
+
* Handle "getEvenAiSessions": return tracked Even AI sessions.
|
|
2169
|
+
*
|
|
2170
|
+
* @param {string} clientId
|
|
2171
|
+
* @returns {Promise<{ unicast: string }>|{ unicast: string }}
|
|
2172
|
+
*/
|
|
2173
|
+
function handleGetEvenAiSessions(clientId) {
|
|
2174
|
+
if (!onGetEvenAiSessions) {
|
|
2175
|
+
return { unicast: formatError("getEvenAiSessions is not available") };
|
|
2176
|
+
}
|
|
2177
|
+
return Promise.resolve(onGetEvenAiSessions()).then(
|
|
2178
|
+
(payload) => ({
|
|
2179
|
+
unicast: formatEvenAiSessions(payload || {}),
|
|
2180
|
+
}),
|
|
2181
|
+
(err) => {
|
|
2182
|
+
logger.error(`[downstream] getEvenAiSessions failed: ${err.message}`);
|
|
2183
|
+
return { unicast: formatEvenAiSessions({ sessions: [] }) };
|
|
2184
|
+
},
|
|
2185
|
+
);
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2188
|
+
/**
|
|
2189
|
+
* Handle "setEvenAiSettings": patch relay-owned Even AI settings.
|
|
2190
|
+
*
|
|
2191
|
+
* @param {string} clientId
|
|
2192
|
+
* @param {object} msg
|
|
2193
|
+
* @returns {Promise<{ unicast: string }>|{ unicast: string }}
|
|
2194
|
+
*/
|
|
2195
|
+
function handleSetEvenAiSettings(clientId, msg) {
|
|
2196
|
+
if (!onSetEvenAiSettings) {
|
|
2197
|
+
return {
|
|
2198
|
+
unicast: formatEvenAiSettingsAck({
|
|
2199
|
+
status: "rejected",
|
|
2200
|
+
error: "setEvenAiSettings is not available",
|
|
2201
|
+
}),
|
|
2202
|
+
};
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2205
|
+
let payload;
|
|
2206
|
+
try {
|
|
2207
|
+
payload = parseSetEvenAiSettings(msg);
|
|
2208
|
+
} catch (err) {
|
|
2209
|
+
return {
|
|
2210
|
+
unicast: formatEvenAiSettingsAck({
|
|
2211
|
+
status: "rejected",
|
|
2212
|
+
error: err && err.message ? err.message : "invalid setEvenAiSettings payload",
|
|
2213
|
+
}),
|
|
2214
|
+
};
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
return Promise.resolve(onSetEvenAiSettings(payload)).then(
|
|
2218
|
+
(result) => ({
|
|
2219
|
+
unicast: formatEvenAiSettingsAck(result || { status: "accepted" }),
|
|
2220
|
+
}),
|
|
2221
|
+
(err) => ({
|
|
2222
|
+
unicast: formatEvenAiSettingsAck({
|
|
2223
|
+
status: "rejected",
|
|
2224
|
+
error: err && err.message ? err.message : "setEvenAiSettings failed",
|
|
2225
|
+
}),
|
|
2226
|
+
}),
|
|
2227
|
+
);
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
/**
|
|
2231
|
+
* Handle "getOcuClawSettings": read relay-owned OcuClaw settings.
|
|
2232
|
+
*
|
|
2233
|
+
* @param {string} clientId
|
|
2234
|
+
* @returns {Promise<{ unicast: string }>|{ unicast: string }}
|
|
2235
|
+
*/
|
|
2236
|
+
function handleGetOcuClawSettings(clientId) {
|
|
2237
|
+
if (!onGetOcuClawSettings) {
|
|
2238
|
+
return { unicast: formatError("getOcuClawSettings is not available") };
|
|
2239
|
+
}
|
|
2240
|
+
return Promise.resolve(onGetOcuClawSettings()).then(
|
|
2241
|
+
(payload) => ({
|
|
2242
|
+
unicast: formatOcuClawSettings(payload || {}),
|
|
2243
|
+
}),
|
|
2244
|
+
(err) => {
|
|
2245
|
+
logger.error(`[downstream] getOcuClawSettings failed: ${err.message}`);
|
|
2246
|
+
return { unicast: formatError(err.message || "getOcuClawSettings failed") };
|
|
2247
|
+
},
|
|
2248
|
+
);
|
|
2249
|
+
}
|
|
2250
|
+
|
|
2251
|
+
/**
|
|
2252
|
+
* Handle "setOcuClawSettings": patch relay-owned OcuClaw settings.
|
|
2253
|
+
*
|
|
2254
|
+
* @param {string} clientId
|
|
2255
|
+
* @param {object} msg
|
|
2256
|
+
* @returns {Promise<{ unicast: string }>|{ unicast: string }}
|
|
2257
|
+
*/
|
|
2258
|
+
function handleSetOcuClawSettings(clientId, msg) {
|
|
2259
|
+
if (!onSetOcuClawSettings) {
|
|
2260
|
+
return {
|
|
2261
|
+
unicast: formatOcuClawSettingsAck({
|
|
2262
|
+
status: "rejected",
|
|
2263
|
+
error: "setOcuClawSettings is not available",
|
|
2264
|
+
}),
|
|
2265
|
+
};
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
let payload;
|
|
2269
|
+
try {
|
|
2270
|
+
payload = parseSetOcuClawSettings(msg);
|
|
2271
|
+
} catch (err) {
|
|
2272
|
+
return {
|
|
2273
|
+
unicast: formatOcuClawSettingsAck({
|
|
2274
|
+
status: "rejected",
|
|
2275
|
+
error: err && err.message ? err.message : "invalid setOcuClawSettings payload",
|
|
2276
|
+
}),
|
|
2277
|
+
};
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
return Promise.resolve(onSetOcuClawSettings(payload)).then(
|
|
2281
|
+
(result) => ({
|
|
2282
|
+
unicast: formatOcuClawSettingsAck(result || { status: "accepted" }),
|
|
2283
|
+
}),
|
|
2284
|
+
(err) => ({
|
|
2285
|
+
unicast: formatOcuClawSettingsAck({
|
|
2286
|
+
status: "rejected",
|
|
2287
|
+
error: err && err.message ? err.message : "setOcuClawSettings failed",
|
|
2288
|
+
}),
|
|
2289
|
+
}),
|
|
2290
|
+
);
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
/**
|
|
2294
|
+
* Handle a "switchSession" message: switch to a different session.
|
|
2295
|
+
*
|
|
2296
|
+
* @param {string} clientId
|
|
2297
|
+
* @param {object} msg - Parsed message with sessionKey
|
|
2298
|
+
* @returns {Promise<{ broadcast: string[] }|null>}
|
|
2299
|
+
*/
|
|
2300
|
+
function handleSwitchSession(clientId, msg) {
|
|
2301
|
+
if (!msg.sessionKey) return null;
|
|
2302
|
+
return onSwitchSession(msg.sessionKey).then(
|
|
2303
|
+
(pages) => ({
|
|
2304
|
+
broadcast: [
|
|
2305
|
+
formatSessionSwitched(msg.sessionKey),
|
|
2306
|
+
formatPages(pages),
|
|
2307
|
+
],
|
|
2308
|
+
}),
|
|
2309
|
+
(err) => {
|
|
2310
|
+
logger.error(`[downstream] switchSession failed: ${err.message}`);
|
|
2311
|
+
return null;
|
|
2312
|
+
},
|
|
2313
|
+
);
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
/**
|
|
2317
|
+
* Handle a "newSession" message: create a new session.
|
|
2318
|
+
*
|
|
2319
|
+
* @param {string} clientId
|
|
2320
|
+
* @returns {Promise<{ broadcast: string[] }|null>}
|
|
2321
|
+
*/
|
|
2322
|
+
function handleNewSession(clientId) {
|
|
2323
|
+
return onNewSession().then(
|
|
2324
|
+
(result) => {
|
|
2325
|
+
const broadcast = [
|
|
2326
|
+
formatSessionSwitched(result.sessionKey),
|
|
2327
|
+
formatPages(result.pages),
|
|
2328
|
+
];
|
|
2329
|
+
if (result && result.sessionModelConfig) {
|
|
2330
|
+
broadcast.push(formatSessionModelConfig(result.sessionModelConfig));
|
|
2331
|
+
}
|
|
2332
|
+
return { broadcast };
|
|
2333
|
+
},
|
|
2334
|
+
(err) => {
|
|
2335
|
+
logger.error(`[downstream] newSession failed: ${err.message}`);
|
|
2336
|
+
return null;
|
|
2337
|
+
},
|
|
2338
|
+
);
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
/**
|
|
2342
|
+
* Handle a "slashCommand" message: forward a slash command to OpenClaw.
|
|
2343
|
+
*
|
|
2344
|
+
* @param {string} clientId
|
|
2345
|
+
* @param {object} msg - Parsed message with command
|
|
2346
|
+
* @returns {Promise<null>}
|
|
2347
|
+
*/
|
|
2348
|
+
function handleSlashCommand(clientId, msg) {
|
|
2349
|
+
if (!msg.command) return null;
|
|
2350
|
+
return onSlashCommand(msg.command).then(
|
|
2351
|
+
() => null,
|
|
2352
|
+
(err) => {
|
|
2353
|
+
logger.error(`[downstream] slashCommand failed: ${err.message}`);
|
|
2354
|
+
return null;
|
|
2355
|
+
},
|
|
2356
|
+
);
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
/**
|
|
2360
|
+
* Handle a "console" message: forward browser console output to log.
|
|
2361
|
+
*
|
|
2362
|
+
* @param {string} clientId
|
|
2363
|
+
* @param {object} msg - Parsed message with level, message
|
|
2364
|
+
* @returns {null}
|
|
2365
|
+
*/
|
|
2366
|
+
function handleConsole(clientId, msg) {
|
|
2367
|
+
if (onConsoleLog) {
|
|
2368
|
+
onConsoleLog(msg.level || "log", msg.message || "");
|
|
2369
|
+
}
|
|
2370
|
+
return null;
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
/**
|
|
2374
|
+
* Handle a structured "eventDebug" message.
|
|
2375
|
+
*
|
|
2376
|
+
* Legacy payloads (without cat/event) fall back to onConsoleLog.
|
|
2377
|
+
*/
|
|
2378
|
+
function handleEventDebug(clientId, msg) {
|
|
2379
|
+
const parsed = parseEventDebug(msg);
|
|
2380
|
+
if (parsed && onEventDebug) {
|
|
2381
|
+
onEventDebug(clientId, parsed);
|
|
2382
|
+
return null;
|
|
2383
|
+
}
|
|
2384
|
+
if (onConsoleLog) {
|
|
2385
|
+
const legacyMessage =
|
|
2386
|
+
typeof msg.data === "string" ? msg.data : JSON.stringify(msg);
|
|
2387
|
+
onConsoleLog("event", legacyMessage);
|
|
2388
|
+
}
|
|
2389
|
+
return null;
|
|
2390
|
+
}
|
|
2391
|
+
|
|
2392
|
+
function handleRemovedListenAction(messageType) {
|
|
2393
|
+
return {
|
|
2394
|
+
unicast: formatError(
|
|
2395
|
+
`${messageType} was removed; hybrid-local voice stays local to the app`,
|
|
2396
|
+
),
|
|
2397
|
+
};
|
|
2398
|
+
}
|
|
2399
|
+
|
|
2400
|
+
/**
|
|
2401
|
+
* Handle a "requestSonioxTemporaryKey" message.
|
|
2402
|
+
*/
|
|
2403
|
+
function handleRequestSonioxTemporaryKey(clientId, msg) {
|
|
2404
|
+
let payload;
|
|
2405
|
+
try {
|
|
2406
|
+
payload = parseRequestSonioxTemporaryKey(msg);
|
|
2407
|
+
} catch (err) {
|
|
2408
|
+
return {
|
|
2409
|
+
unicast: formatSonioxTemporaryKeyError({
|
|
2410
|
+
voiceSessionId:
|
|
2411
|
+
msg && typeof msg.voiceSessionId === "string" ? msg.voiceSessionId : "",
|
|
2412
|
+
error: err && err.message ? err.message : "requestSonioxTemporaryKey failed",
|
|
2413
|
+
code: normalizeSonioxTemporaryKeyErrorCode(err),
|
|
2414
|
+
}),
|
|
2415
|
+
};
|
|
2416
|
+
}
|
|
2417
|
+
|
|
2418
|
+
if (!onRequestSonioxTemporaryKey) {
|
|
2419
|
+
return {
|
|
2420
|
+
unicast: formatSonioxTemporaryKeyError({
|
|
2421
|
+
voiceSessionId: payload.voiceSessionId,
|
|
2422
|
+
error: "requestSonioxTemporaryKey is not available",
|
|
2423
|
+
code: "soniox_temp_key_unavailable",
|
|
2424
|
+
}),
|
|
2425
|
+
};
|
|
2426
|
+
}
|
|
2427
|
+
|
|
2428
|
+
try {
|
|
2429
|
+
const result = onRequestSonioxTemporaryKey(clientId, payload);
|
|
2430
|
+
if (result && typeof result.then === "function") {
|
|
2431
|
+
return result.then(
|
|
2432
|
+
(resolved) => ({ unicast: formatSonioxTemporaryKey(resolved || payload) }),
|
|
2433
|
+
(err) => {
|
|
2434
|
+
const error =
|
|
2435
|
+
err && err.message
|
|
2436
|
+
? err.message
|
|
2437
|
+
: "requestSonioxTemporaryKey failed";
|
|
2438
|
+
return {
|
|
2439
|
+
unicast: formatSonioxTemporaryKeyError({
|
|
2440
|
+
voiceSessionId: payload.voiceSessionId,
|
|
2441
|
+
error,
|
|
2442
|
+
code: normalizeSonioxTemporaryKeyErrorCode(err),
|
|
2443
|
+
}),
|
|
2444
|
+
};
|
|
2445
|
+
},
|
|
2446
|
+
);
|
|
2447
|
+
}
|
|
2448
|
+
return { unicast: formatSonioxTemporaryKey(result || payload) };
|
|
2449
|
+
} catch (err) {
|
|
2450
|
+
return {
|
|
2451
|
+
unicast: formatSonioxTemporaryKeyError({
|
|
2452
|
+
voiceSessionId: payload.voiceSessionId,
|
|
2453
|
+
error: err && err.message ? err.message : "requestSonioxTemporaryKey failed",
|
|
2454
|
+
code: normalizeSonioxTemporaryKeyErrorCode(err),
|
|
2455
|
+
}),
|
|
2456
|
+
};
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2460
|
+
/**
|
|
2461
|
+
* Handle a "debug-set" message: enable/disable debug categories with TTL.
|
|
2462
|
+
*/
|
|
2463
|
+
function handleDebugSet(clientId, msg) {
|
|
2464
|
+
if (!onDebugSet) {
|
|
2465
|
+
return { unicast: formatError("debug-set is not available") };
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
let payload;
|
|
2469
|
+
try {
|
|
2470
|
+
payload = parseDebugSet(msg);
|
|
2471
|
+
} catch (err) {
|
|
2472
|
+
return { unicast: formatError(err.message) };
|
|
2473
|
+
}
|
|
2474
|
+
|
|
2475
|
+
try {
|
|
2476
|
+
const result = onDebugSet(clientId, payload);
|
|
2477
|
+
if (result && typeof result.then === "function") {
|
|
2478
|
+
return result.then(
|
|
2479
|
+
(resolved) => {
|
|
2480
|
+
const payloadResult = resolved || { ok: true };
|
|
2481
|
+
const out = { unicast: formatDebugSet(payloadResult) };
|
|
2482
|
+
if (
|
|
2483
|
+
payloadResult.ok !== false &&
|
|
2484
|
+
Number.isFinite(payloadResult.nowMs) &&
|
|
2485
|
+
Array.isArray(payloadResult.enabled)
|
|
2486
|
+
) {
|
|
2487
|
+
out.broadcastApp = formatDebugConfigSnapshot({
|
|
2488
|
+
serverNowMs: payloadResult.nowMs,
|
|
2489
|
+
enabled: payloadResult.enabled,
|
|
2490
|
+
});
|
|
2491
|
+
}
|
|
2492
|
+
return out;
|
|
2493
|
+
},
|
|
2494
|
+
(err) => ({ unicast: formatError(err.message || "debug-set failed") }),
|
|
2495
|
+
);
|
|
2496
|
+
}
|
|
2497
|
+
const payloadResult = result || { ok: true };
|
|
2498
|
+
const out = { unicast: formatDebugSet(payloadResult) };
|
|
2499
|
+
if (
|
|
2500
|
+
payloadResult.ok !== false &&
|
|
2501
|
+
Number.isFinite(payloadResult.nowMs) &&
|
|
2502
|
+
Array.isArray(payloadResult.enabled)
|
|
2503
|
+
) {
|
|
2504
|
+
out.broadcastApp = formatDebugConfigSnapshot({
|
|
2505
|
+
serverNowMs: payloadResult.nowMs,
|
|
2506
|
+
enabled: payloadResult.enabled,
|
|
2507
|
+
});
|
|
2508
|
+
}
|
|
2509
|
+
return out;
|
|
2510
|
+
} catch (err) {
|
|
2511
|
+
return { unicast: formatError(err.message || "debug-set failed") };
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
/**
|
|
2516
|
+
* Handle a "debug-dump" message: fetch structured debug events.
|
|
2517
|
+
*/
|
|
2518
|
+
function handleDebugDump(clientId, msg) {
|
|
2519
|
+
if (!onDebugDump) {
|
|
2520
|
+
return { unicast: formatError("debug-dump is not available") };
|
|
2521
|
+
}
|
|
2522
|
+
|
|
2523
|
+
let payload;
|
|
2524
|
+
try {
|
|
2525
|
+
payload = parseDebugDump(msg);
|
|
2526
|
+
} catch (err) {
|
|
2527
|
+
return { unicast: formatError(err.message) };
|
|
2528
|
+
}
|
|
2529
|
+
|
|
2530
|
+
try {
|
|
2531
|
+
const result = onDebugDump(clientId, payload);
|
|
2532
|
+
if (result && typeof result.then === "function") {
|
|
2533
|
+
return result.then(
|
|
2534
|
+
(resolved) => ({ unicast: formatDebugDump(resolved || { ok: true, events: [] }) }),
|
|
2535
|
+
(err) => ({ unicast: formatError(err.message || "debug-dump failed") }),
|
|
2536
|
+
);
|
|
2537
|
+
}
|
|
2538
|
+
return { unicast: formatDebugDump(result || { ok: true, events: [] }) };
|
|
2539
|
+
} catch (err) {
|
|
2540
|
+
return { unicast: formatError(err.message || "debug-dump failed") };
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
|
|
2544
|
+
/**
|
|
2545
|
+
* Handle a "remote-control" message: dispatch remote actions to app clients.
|
|
2546
|
+
*/
|
|
2547
|
+
function handleRemoteControl(clientId, msg) {
|
|
2548
|
+
if (!onRemoteControl) {
|
|
2549
|
+
return { unicast: formatError("remote-control is not available") };
|
|
2550
|
+
}
|
|
2551
|
+
|
|
2552
|
+
let payload;
|
|
2553
|
+
try {
|
|
2554
|
+
payload = parseRemoteControl(msg);
|
|
2555
|
+
} catch (err) {
|
|
2556
|
+
return { unicast: formatError(err.message) };
|
|
2557
|
+
}
|
|
2558
|
+
|
|
2559
|
+
const finalize = (result) => {
|
|
2560
|
+
const resolved = result || {};
|
|
2561
|
+
const requestId = resolved.requestId || payload.requestId || null;
|
|
2562
|
+
const ack = {
|
|
2563
|
+
ok: resolved.ok !== false,
|
|
2564
|
+
requestId,
|
|
2565
|
+
action: payload.action,
|
|
2566
|
+
dispatched: !!resolved.control,
|
|
2567
|
+
};
|
|
2568
|
+
if (resolved.message) ack.message = resolved.message;
|
|
2569
|
+
if (resolved.detail) ack.detail = resolved.detail;
|
|
2570
|
+
|
|
2571
|
+
const out = {
|
|
2572
|
+
unicast: formatRemoteControlAck(ack),
|
|
2573
|
+
};
|
|
2574
|
+
if (resolved.control) {
|
|
2575
|
+
out.broadcast = formatRemoteControl(resolved.control);
|
|
2576
|
+
}
|
|
2577
|
+
return out;
|
|
2578
|
+
};
|
|
2579
|
+
|
|
2580
|
+
try {
|
|
2581
|
+
const result = onRemoteControl(clientId, payload);
|
|
2582
|
+
if (result && typeof result.then === "function") {
|
|
2583
|
+
return result.then(
|
|
2584
|
+
(resolved) => finalize(resolved),
|
|
2585
|
+
(err) => ({ unicast: formatError(err.message || "remote-control failed") }),
|
|
2586
|
+
);
|
|
2587
|
+
}
|
|
2588
|
+
return finalize(result);
|
|
2589
|
+
} catch (err) {
|
|
2590
|
+
return { unicast: formatError(err.message || "remote-control failed") };
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2593
|
+
|
|
2594
|
+
// --- Public API ---
|
|
2595
|
+
|
|
2596
|
+
return {
|
|
2597
|
+
/**
|
|
2598
|
+
* Process an incoming downstream message.
|
|
2599
|
+
*
|
|
2600
|
+
* @param {string} clientId - Identifier of the sending client
|
|
2601
|
+
* @param {string} raw - Raw JSON string
|
|
2602
|
+
* @returns {{ unicast: string }|{ broadcast: string }|null|Promise}
|
|
2603
|
+
*/
|
|
2604
|
+
handleMessage(clientId, raw) {
|
|
2605
|
+
let msg;
|
|
2606
|
+
try {
|
|
2607
|
+
msg = JSON.parse(raw);
|
|
2608
|
+
} catch {
|
|
2609
|
+
return { unicast: formatError("Invalid JSON") };
|
|
2610
|
+
}
|
|
2611
|
+
|
|
2612
|
+
if (
|
|
2613
|
+
!externalDebugToolsEnabled &&
|
|
2614
|
+
msg &&
|
|
2615
|
+
typeof msg.type === "string" &&
|
|
2616
|
+
isExternalDebugToolMessageType(msg.type)
|
|
2617
|
+
) {
|
|
2618
|
+
return {
|
|
2619
|
+
unicast: formatError(EXTERNAL_DEBUG_TOOLS_DISABLED_ERROR),
|
|
2620
|
+
};
|
|
2621
|
+
}
|
|
2622
|
+
|
|
2623
|
+
switch (msg.type) {
|
|
2624
|
+
case APP_PROTOCOL.messageSend:
|
|
2625
|
+
return handleSend(clientId, msg);
|
|
2626
|
+
case "simulate":
|
|
2627
|
+
return handleSimulate(clientId, msg);
|
|
2628
|
+
case "simulateStream":
|
|
2629
|
+
return handleSimulateStream(clientId, msg);
|
|
2630
|
+
case APP_PROTOCOL.protocolSubscribe:
|
|
2631
|
+
return handleSubscribeProtocol(clientId);
|
|
2632
|
+
case APP_PROTOCOL.approvalResolve:
|
|
2633
|
+
return handleApprovalResponse(clientId, msg);
|
|
2634
|
+
case APP_PROTOCOL.sessionReset:
|
|
2635
|
+
return handleNewChat(clientId);
|
|
2636
|
+
case APP_PROTOCOL.sessionList:
|
|
2637
|
+
return handleGetSessions(clientId);
|
|
2638
|
+
case APP_PROTOCOL.sessionSwitch:
|
|
2639
|
+
return handleSwitchSession(clientId, msg);
|
|
2640
|
+
case APP_PROTOCOL.sessionCreate:
|
|
2641
|
+
return handleNewSession(clientId);
|
|
2642
|
+
case APP_PROTOCOL.modelCatalogGet:
|
|
2643
|
+
return handleGetModelsCatalog(clientId);
|
|
2644
|
+
case APP_PROTOCOL.skillsCatalogGet:
|
|
2645
|
+
case "getSkills":
|
|
2646
|
+
return handleGetSkillsCatalog(clientId);
|
|
2647
|
+
case APP_PROTOCOL.sonioxModelsGet:
|
|
2648
|
+
case "getSonioxModels":
|
|
2649
|
+
return handleGetSonioxModels(clientId);
|
|
2650
|
+
case APP_PROTOCOL.statusGet:
|
|
2651
|
+
case "getStatus":
|
|
2652
|
+
return handleGetStatus(clientId);
|
|
2653
|
+
case APP_PROTOCOL.sessionConfigGet:
|
|
2654
|
+
return handleGetSessionModelConfig(clientId);
|
|
2655
|
+
case APP_PROTOCOL.sessionConfigSet:
|
|
2656
|
+
return handleSetSessionModelConfig(clientId, msg);
|
|
2657
|
+
case APP_PROTOCOL.evenAiSettingsGet:
|
|
2658
|
+
return handleGetEvenAiSettings(clientId);
|
|
2659
|
+
case APP_PROTOCOL.evenAiSessionList:
|
|
2660
|
+
return handleGetEvenAiSessions(clientId);
|
|
2661
|
+
case APP_PROTOCOL.evenAiSettingsSet:
|
|
2662
|
+
return handleSetEvenAiSettings(clientId, msg);
|
|
2663
|
+
case APP_PROTOCOL.ocuClawSettingsGet:
|
|
2664
|
+
return handleGetOcuClawSettings(clientId);
|
|
2665
|
+
case APP_PROTOCOL.ocuClawSettingsSet:
|
|
2666
|
+
return handleSetOcuClawSettings(clientId, msg);
|
|
2667
|
+
case APP_PROTOCOL.commandSlash:
|
|
2668
|
+
return handleSlashCommand(clientId, msg);
|
|
2669
|
+
case "console":
|
|
2670
|
+
return handleConsole(clientId, msg);
|
|
2671
|
+
case "listen-start":
|
|
2672
|
+
case "listen-stop":
|
|
2673
|
+
case "listen-send":
|
|
2674
|
+
case "listen-retry":
|
|
2675
|
+
return handleRemovedListenAction(msg.type);
|
|
2676
|
+
case APP_PROTOCOL.requestSonioxTemporaryKey:
|
|
2677
|
+
return handleRequestSonioxTemporaryKey(clientId, msg);
|
|
2678
|
+
case "debug-set":
|
|
2679
|
+
return handleDebugSet(clientId, msg);
|
|
2680
|
+
case "debug-dump":
|
|
2681
|
+
return handleDebugDump(clientId, msg);
|
|
2682
|
+
case "remote-control":
|
|
2683
|
+
return handleRemoteControl(clientId, msg);
|
|
2684
|
+
case APP_PROTOCOL.debugEvent:
|
|
2685
|
+
return handleEventDebug(clientId, msg);
|
|
2686
|
+
default:
|
|
2687
|
+
return null;
|
|
2688
|
+
}
|
|
2689
|
+
},
|
|
2690
|
+
|
|
2691
|
+
formatPages,
|
|
2692
|
+
formatStatus,
|
|
2693
|
+
formatActivity,
|
|
2694
|
+
formatSendAck,
|
|
2695
|
+
formatProtocol,
|
|
2696
|
+
formatStreaming,
|
|
2697
|
+
formatSessions,
|
|
2698
|
+
formatSessionSwitched,
|
|
2699
|
+
formatModelsCatalog,
|
|
2700
|
+
formatSkillsCatalog,
|
|
2701
|
+
formatSonioxModels,
|
|
2702
|
+
formatSessionModelConfig,
|
|
2703
|
+
formatSessionModelConfigAck,
|
|
2704
|
+
formatEvenAiSettings,
|
|
2705
|
+
formatOcuClawSettings,
|
|
2706
|
+
formatEvenAiSessions,
|
|
2707
|
+
formatEvenAiSettingsAck,
|
|
2708
|
+
formatOcuClawSettingsAck,
|
|
2709
|
+
formatApproval,
|
|
2710
|
+
formatApprovalResolved,
|
|
2711
|
+
formatApprovalResponseAck,
|
|
2712
|
+
formatTranscription,
|
|
2713
|
+
formatListenCommitted,
|
|
2714
|
+
formatListenEnded,
|
|
2715
|
+
formatListenError,
|
|
2716
|
+
formatListenReady,
|
|
2717
|
+
formatSonioxTemporaryKey,
|
|
2718
|
+
formatSonioxTemporaryKeyError,
|
|
2719
|
+
formatDebugSet,
|
|
2720
|
+
formatDebugDump,
|
|
2721
|
+
formatDebugConfigSnapshot,
|
|
2722
|
+
formatRemoteControl,
|
|
2723
|
+
formatRemoteControlAck,
|
|
2724
|
+
formatError,
|
|
2725
|
+
|
|
2726
|
+
/**
|
|
2727
|
+
* Check whether a client is subscribed to protocol frame forwarding.
|
|
2728
|
+
*
|
|
2729
|
+
* @param {string} clientId
|
|
2730
|
+
* @returns {boolean}
|
|
2731
|
+
*/
|
|
2732
|
+
isProtocolSubscriber(clientId) {
|
|
2733
|
+
return protocolSubscribers.has(clientId);
|
|
2734
|
+
},
|
|
2735
|
+
|
|
2736
|
+
/**
|
|
2737
|
+
* Clean up state for a disconnected client.
|
|
2738
|
+
*
|
|
2739
|
+
* @param {string} clientId
|
|
2740
|
+
*/
|
|
2741
|
+
removeClient(clientId) {
|
|
2742
|
+
protocolSubscribers.delete(clientId);
|
|
2743
|
+
},
|
|
2744
|
+
};
|
|
2745
|
+
}
|
|
2746
|
+
|
|
2747
|
+
export { createDownstreamHandler };
|