@brantrusnak/openclaw-omadeus 1.0.4 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/api/message.api.js +13 -2
- package/dist/src/channel.js +38 -11
- package/dist/src/config.js +9 -4
- package/dist/src/defaults.js +30 -4
- package/dist/src/inbound-policy.js +11 -14
- package/dist/src/message-handler.js +14 -2
- package/dist/src/onboarding.js +85 -56
- package/dist/src/outbound.js +10 -1
- package/dist/src/sent-message-tracker.js +116 -0
- package/dist/src/setup-core.js +4 -4
- package/openclaw.plugin.json +22 -11
- package/package.json +1 -1
- package/src/api/message.api.ts +4 -2
- package/src/channel.ts +43 -7
- package/src/config.ts +14 -4
- package/src/defaults.ts +44 -2
- package/src/inbound-policy.ts +24 -13
- package/src/message-handler.ts +35 -7
- package/src/onboarding.ts +136 -60
- package/src/outbound.ts +14 -2
- package/src/sent-message-tracker.ts +155 -0
- package/src/setup-core.ts +4 -4
- package/src/types.ts +6 -2
package/src/onboarding.ts
CHANGED
|
@@ -7,12 +7,19 @@ import {
|
|
|
7
7
|
import { listMemberChannelViews } from "./api/channel.api.js";
|
|
8
8
|
import { authenticate } from "./auth.js";
|
|
9
9
|
import { getOmadeusChannelConfig, resolveOmadeusAccount } from "./config.js";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
getOmadeusEnvironmentUrls,
|
|
12
|
+
OMADEUS_DEFAULT_ENVIRONMENT,
|
|
13
|
+
OMADEUS_ENVIRONMENTS,
|
|
14
|
+
resolveOmadeusEnvironment,
|
|
15
|
+
type OmadeusEnvironment,
|
|
16
|
+
} from "./defaults.js";
|
|
11
17
|
import { formatMemberLabel } from "./member-resolve.js";
|
|
12
18
|
import type {
|
|
13
19
|
OmadeusChannelConfig,
|
|
14
20
|
OmadeusChannelView,
|
|
15
21
|
OmadeusInboundEntityKind,
|
|
22
|
+
OmadeusInboundMentionPolicy,
|
|
16
23
|
OmadeusOrganizationMember,
|
|
17
24
|
} from "./types.js";
|
|
18
25
|
import { OMADEUS_INBOUND_ENTITY_KINDS } from "./types.js";
|
|
@@ -41,21 +48,38 @@ function formatAuthError(err: unknown): string {
|
|
|
41
48
|
return parts.join(" — ");
|
|
42
49
|
}
|
|
43
50
|
|
|
44
|
-
async function noteOmadeusAuthHelp(
|
|
51
|
+
async function noteOmadeusAuthHelp(
|
|
52
|
+
prompter: WizardPrompter,
|
|
53
|
+
environment: OmadeusEnvironment,
|
|
54
|
+
): Promise<void> {
|
|
55
|
+
const envLabel = OMADEUS_ENVIRONMENTS[environment].label;
|
|
45
56
|
await prompter.note(
|
|
46
57
|
[
|
|
47
|
-
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
" - Organization ID (we can look it up for you)",
|
|
51
|
-
`CAS URL: ${OMADEUS_CAS_URL}`,
|
|
52
|
-
`Maestro URL: ${OMADEUS_MAESTRO_URL}`,
|
|
53
|
-
"Env vars supported: OMADEUS_EMAIL, OMADEUS_PASSWORD, OMADEUS_ORGANIZATION_ID.",
|
|
58
|
+
`Connect OpenClaw to Omadeus (${envLabel}).`,
|
|
59
|
+
"",
|
|
60
|
+
"We'll ask for your email and password, then show the organizations on your account so you can pick one.",
|
|
54
61
|
].join("\n"),
|
|
55
62
|
"Omadeus setup",
|
|
56
63
|
);
|
|
57
64
|
}
|
|
58
65
|
|
|
66
|
+
async function promptEnvironment(
|
|
67
|
+
prompter: WizardPrompter,
|
|
68
|
+
existing?: OmadeusEnvironment,
|
|
69
|
+
): Promise<OmadeusEnvironment> {
|
|
70
|
+
const initial = existing ?? OMADEUS_DEFAULT_ENVIRONMENT;
|
|
71
|
+
const choice = await prompter.select({
|
|
72
|
+
message: "Select Omadeus environment",
|
|
73
|
+
options: (Object.keys(OMADEUS_ENVIRONMENTS) as OmadeusEnvironment[]).map((env) => ({
|
|
74
|
+
value: env,
|
|
75
|
+
label: OMADEUS_ENVIRONMENTS[env].label,
|
|
76
|
+
hint: getOmadeusEnvironmentUrls(env).maestroUrl,
|
|
77
|
+
})),
|
|
78
|
+
initialValue: initial,
|
|
79
|
+
});
|
|
80
|
+
return resolveOmadeusEnvironment(choice);
|
|
81
|
+
}
|
|
82
|
+
|
|
59
83
|
async function promptOrganizationId(params: {
|
|
60
84
|
prompter: WizardPrompter;
|
|
61
85
|
maestroUrl: string;
|
|
@@ -226,36 +250,68 @@ async function promptCredentials(
|
|
|
226
250
|
return { email, password };
|
|
227
251
|
}
|
|
228
252
|
|
|
229
|
-
|
|
253
|
+
/**
|
|
254
|
+
* Prompt for the set of users allowed to message this OpenClaw instance.
|
|
255
|
+
*
|
|
256
|
+
* This single allowlist governs direct messages, channels, and entity rooms.
|
|
257
|
+
* There is no "all users" option: only whitelisted members may message
|
|
258
|
+
* OpenClaw. The logged-in user is always added
|
|
259
|
+
* to the allowlist (and is excluded from the selectable list by the caller) so
|
|
260
|
+
* they can interact with their own OpenClaw. Self-authored echoes (the reply
|
|
261
|
+
* loop) are filtered earlier, at socket ingestion, by the SentMessageTracker —
|
|
262
|
+
* not by the inbound policy — so allowing yourself here cannot cause a loop.
|
|
263
|
+
*/
|
|
264
|
+
async function promptMessagingAllowlist(params: {
|
|
230
265
|
prompter: WizardPrompter;
|
|
231
|
-
message: string;
|
|
232
266
|
members: OmadeusOrganizationMember[];
|
|
267
|
+
selfReferenceId: number;
|
|
233
268
|
existingReferenceIds?: number[];
|
|
234
|
-
}): Promise<number[]
|
|
235
|
-
const { prompter,
|
|
269
|
+
}): Promise<number[]> {
|
|
270
|
+
const { prompter, members, selfReferenceId, existingReferenceIds } = params;
|
|
271
|
+
|
|
272
|
+
let selected: number[] = [];
|
|
236
273
|
if (members.length === 0) {
|
|
237
|
-
|
|
274
|
+
await prompter.note(
|
|
275
|
+
"No other organization members found. Only you will be able to message OpenClaw.",
|
|
276
|
+
"Omadeus messaging allowlist",
|
|
277
|
+
);
|
|
278
|
+
} else {
|
|
279
|
+
const memberReferenceIds = new Set(members.map((member) => member.referenceId));
|
|
280
|
+
const initialValues = (existingReferenceIds ?? [])
|
|
281
|
+
.filter((id) => id !== selfReferenceId && memberReferenceIds.has(id))
|
|
282
|
+
.map(String);
|
|
283
|
+
const chosen = await promptMultiSelect({
|
|
284
|
+
prompter,
|
|
285
|
+
message: "Which users do you want to be able to message this OpenClaw instance? (You are always allowed.)",
|
|
286
|
+
options: memberOptions(members),
|
|
287
|
+
initialValues,
|
|
288
|
+
});
|
|
289
|
+
selected = readReferenceIds(chosen);
|
|
238
290
|
}
|
|
239
291
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
{ value: "all", label: "All users", hint: "No sender allowlist" },
|
|
244
|
-
{ value: "specific", label: "Specific users", hint: "Select one or more users" },
|
|
245
|
-
],
|
|
246
|
-
initialValue: existingReferenceIds && existingReferenceIds.length > 0 ? "specific" : "all",
|
|
247
|
-
});
|
|
248
|
-
if (mode === "all") {
|
|
249
|
-
return undefined;
|
|
250
|
-
}
|
|
292
|
+
// Always allow the logged-in user so they can message their own OpenClaw.
|
|
293
|
+
return Array.from(new Set([selfReferenceId, ...selected]));
|
|
294
|
+
}
|
|
251
295
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
296
|
+
/**
|
|
297
|
+
* Ask whether an @mention is required to trigger OpenClaw in a given surface
|
|
298
|
+
* (channels or entity rooms). DMs never use this — you can't @mention in a DM.
|
|
299
|
+
*/
|
|
300
|
+
async function promptRequireMention(params: {
|
|
301
|
+
prompter: WizardPrompter;
|
|
302
|
+
surfaceLabel: string;
|
|
303
|
+
existing?: OmadeusInboundMentionPolicy;
|
|
304
|
+
}): Promise<OmadeusInboundMentionPolicy> {
|
|
305
|
+
// Preserve an existing "outsideAllowlist" policy so rerunning onboarding does
|
|
306
|
+
// not force previously allowlisted users to start @mentioning OpenClaw.
|
|
307
|
+
if (params.existing === "outsideAllowlist") {
|
|
308
|
+
return "outsideAllowlist";
|
|
309
|
+
}
|
|
310
|
+
const required = await params.prompter.confirm({
|
|
311
|
+
message: `Require an @mention to trigger OpenClaw in ${params.surfaceLabel}?`,
|
|
312
|
+
initialValue: params.existing ? params.existing !== "never" : true,
|
|
257
313
|
});
|
|
258
|
-
return
|
|
314
|
+
return required ? "always" : "never";
|
|
259
315
|
}
|
|
260
316
|
|
|
261
317
|
async function promptEntityKindSelection(params: {
|
|
@@ -315,16 +371,19 @@ export const omadeusSetupWizard: ChannelSetupWizard = {
|
|
|
315
371
|
const section = getOmadeusChannelConfig(cfg) ?? {};
|
|
316
372
|
let next = cfg;
|
|
317
373
|
|
|
374
|
+
const environment = await promptEnvironment(
|
|
375
|
+
prompter,
|
|
376
|
+
section.environment ? resolveOmadeusEnvironment(section.environment) : undefined,
|
|
377
|
+
);
|
|
378
|
+
const { casUrl, maestroUrl } = getOmadeusEnvironmentUrls(environment);
|
|
379
|
+
|
|
318
380
|
if (account.credentialSource === "none") {
|
|
319
|
-
await noteOmadeusAuthHelp(prompter);
|
|
381
|
+
await noteOmadeusAuthHelp(prompter, environment);
|
|
320
382
|
}
|
|
321
383
|
|
|
322
384
|
const envEmail = process.env.OMADEUS_EMAIL?.trim();
|
|
323
385
|
const envPassword = process.env.OMADEUS_PASSWORD?.trim();
|
|
324
386
|
|
|
325
|
-
const casUrl = OMADEUS_CAS_URL;
|
|
326
|
-
const maestroUrl = OMADEUS_MAESTRO_URL;
|
|
327
|
-
|
|
328
387
|
let { email, password } = await promptCredentials(prompter, {
|
|
329
388
|
email: section.email ?? envEmail,
|
|
330
389
|
password: section.password ?? envPassword,
|
|
@@ -388,11 +447,17 @@ export const omadeusSetupWizard: ChannelSetupWizard = {
|
|
|
388
447
|
});
|
|
389
448
|
const existingInbound = section.inbound;
|
|
390
449
|
|
|
391
|
-
const
|
|
450
|
+
const allowedUserReferenceIds = await promptMessagingAllowlist({
|
|
392
451
|
prompter,
|
|
393
|
-
message: "Which users can DM OpenClaw directly?",
|
|
394
452
|
members,
|
|
395
|
-
|
|
453
|
+
selfReferenceId,
|
|
454
|
+
existingReferenceIds: Array.from(
|
|
455
|
+
new Set([
|
|
456
|
+
...(existingInbound?.direct?.allowedSenderReferenceIds ?? []),
|
|
457
|
+
...(existingInbound?.channels?.allowedSenderReferenceIds ?? []),
|
|
458
|
+
...(existingInbound?.entities?.allowedSenderReferenceIds ?? []),
|
|
459
|
+
]),
|
|
460
|
+
),
|
|
396
461
|
});
|
|
397
462
|
|
|
398
463
|
const selectedChannels = await promptChannelSelection({
|
|
@@ -403,30 +468,32 @@ export const omadeusSetupWizard: ChannelSetupWizard = {
|
|
|
403
468
|
existingChannelViewIds: existingInbound?.channels?.allowedChannelViewIds,
|
|
404
469
|
});
|
|
405
470
|
|
|
406
|
-
|
|
471
|
+
// Channels reuse the same messaging allowlist as direct messages.
|
|
472
|
+
const channelSenderIds = selectedChannels.length > 0 ? allowedUserReferenceIds : undefined;
|
|
473
|
+
const channelRequireMention =
|
|
407
474
|
selectedChannels.length > 0
|
|
408
|
-
? await
|
|
475
|
+
? await promptRequireMention({
|
|
409
476
|
prompter,
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
existingReferenceIds: existingInbound?.channels?.allowedSenderReferenceIds,
|
|
477
|
+
surfaceLabel: "allowed channels",
|
|
478
|
+
existing: existingInbound?.channels?.requireMention,
|
|
413
479
|
})
|
|
414
|
-
:
|
|
480
|
+
: "never";
|
|
415
481
|
|
|
416
482
|
const entityKinds = await promptEntityKindSelection({
|
|
417
483
|
prompter,
|
|
418
484
|
existingKinds: existingInbound?.entities?.allowedKinds,
|
|
419
485
|
});
|
|
420
486
|
|
|
421
|
-
|
|
487
|
+
// Entity rooms reuse the same messaging allowlist as direct messages.
|
|
488
|
+
const entitySenderIds = entityKinds.length > 0 ? allowedUserReferenceIds : undefined;
|
|
489
|
+
const entityRequireMention =
|
|
422
490
|
entityKinds.length > 0
|
|
423
|
-
? await
|
|
491
|
+
? await promptRequireMention({
|
|
424
492
|
prompter,
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
existingReferenceIds: existingInbound?.entities?.allowedSenderReferenceIds,
|
|
493
|
+
surfaceLabel: "entity rooms",
|
|
494
|
+
existing: existingInbound?.entities?.requireMention,
|
|
428
495
|
})
|
|
429
|
-
:
|
|
496
|
+
: "never";
|
|
430
497
|
|
|
431
498
|
const channelRoomIds = selectedChannels
|
|
432
499
|
.flatMap((selectedChannel) => [
|
|
@@ -441,20 +508,29 @@ export const omadeusSetupWizard: ChannelSetupWizard = {
|
|
|
441
508
|
|
|
442
509
|
const senderSummary = (ids: number[] | undefined) =>
|
|
443
510
|
ids && ids.length > 0 ? ids.join(", ") : "all users";
|
|
444
|
-
const
|
|
445
|
-
|
|
511
|
+
const mentionSummary = (require: OmadeusInboundMentionPolicy) =>
|
|
512
|
+
require === "never"
|
|
513
|
+
? "no @mention required"
|
|
514
|
+
: require === "outsideAllowlist"
|
|
515
|
+
? "@mention required outside the allowlist"
|
|
516
|
+
: "@mention required";
|
|
446
517
|
|
|
447
518
|
const channelSummary =
|
|
448
519
|
selectedChannels.length > 0
|
|
449
|
-
? `- Channels "${channelTitles}": rooms ${channelRoomIds.join(", ") || "(no room ids)"} from ${senderSummary(channelSenderIds)};
|
|
520
|
+
? `- Channels "${channelTitles}": rooms ${channelRoomIds.join(", ") || "(no room ids)"} from ${senderSummary(channelSenderIds)}; ${mentionSummary(channelRequireMention)}.`
|
|
450
521
|
: "- Channels: disabled (none selected).";
|
|
451
522
|
|
|
523
|
+
const entitySummary =
|
|
524
|
+
entityKinds.length > 0
|
|
525
|
+
? `- Entity rooms (${entityKinds.join(", ")}): ${senderSummary(entitySenderIds)}; ${mentionSummary(entityRequireMention)}.`
|
|
526
|
+
: "- Entity rooms: disabled (no room types selected).";
|
|
527
|
+
|
|
452
528
|
await prompter.note(
|
|
453
529
|
[
|
|
454
530
|
`Inbound policy (Jaguar chat):`,
|
|
455
|
-
`- Direct messages: enabled for ${senderSummary(
|
|
531
|
+
`- Direct messages: enabled for ${senderSummary(allowedUserReferenceIds)}.`,
|
|
456
532
|
channelSummary,
|
|
457
|
-
|
|
533
|
+
entitySummary,
|
|
458
534
|
].join("\n"),
|
|
459
535
|
"Omadeus inbound policy",
|
|
460
536
|
);
|
|
@@ -465,17 +541,17 @@ export const omadeusSetupWizard: ChannelSetupWizard = {
|
|
|
465
541
|
...next.channels,
|
|
466
542
|
omadeus: {
|
|
467
543
|
enabled: true,
|
|
468
|
-
|
|
469
|
-
maestroUrl,
|
|
544
|
+
environment,
|
|
470
545
|
email,
|
|
471
546
|
password,
|
|
472
547
|
organizationId,
|
|
473
548
|
sessionToken,
|
|
549
|
+
sessionTokenEnvironment: environment,
|
|
474
550
|
inbound: {
|
|
475
551
|
version: 1,
|
|
476
552
|
direct: {
|
|
477
553
|
enabled: true,
|
|
478
|
-
|
|
554
|
+
allowedSenderReferenceIds: allowedUserReferenceIds,
|
|
479
555
|
requireMention: "never",
|
|
480
556
|
},
|
|
481
557
|
channels: {
|
|
@@ -483,13 +559,13 @@ export const omadeusSetupWizard: ChannelSetupWizard = {
|
|
|
483
559
|
allowedRoomIds: channelRoomIds,
|
|
484
560
|
allowedChannelViewIds: channelViewIds,
|
|
485
561
|
...(channelSenderIds ? { allowedSenderReferenceIds: channelSenderIds } : {}),
|
|
486
|
-
requireMention:
|
|
562
|
+
requireMention: channelRequireMention,
|
|
487
563
|
},
|
|
488
564
|
entities: {
|
|
489
565
|
enabled: entityKinds.length > 0,
|
|
490
566
|
allowedKinds: entityKinds,
|
|
491
567
|
...(entitySenderIds ? { allowedSenderReferenceIds: entitySenderIds } : {}),
|
|
492
|
-
requireMention:
|
|
568
|
+
requireMention: entityRequireMention,
|
|
493
569
|
},
|
|
494
570
|
},
|
|
495
571
|
},
|
package/src/outbound.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { sendRoomMessage } from "./api/message.api.js";
|
|
2
|
+
import type { SentMessageTracker } from "./sent-message-tracker.js";
|
|
2
3
|
import type { JaguarSocketClient } from "./socket/jaguar.socket.js";
|
|
3
|
-
import type
|
|
4
|
+
import { generateTemporaryId, type OmadeusApiOptions } from "./utils/http.util.js";
|
|
4
5
|
|
|
5
6
|
export type OutboundDeps = {
|
|
6
7
|
apiOpts: OmadeusApiOptions;
|
|
7
8
|
jaguarSocket: JaguarSocketClient;
|
|
9
|
+
/** Records messages we send so their socket echoes can be suppressed. */
|
|
10
|
+
sentTracker?: SentMessageTracker;
|
|
8
11
|
};
|
|
9
12
|
|
|
10
13
|
export async function sendOmadeusMessage(
|
|
@@ -13,11 +16,20 @@ export async function sendOmadeusMessage(
|
|
|
13
16
|
): Promise<{ channel: string; messageId: string; chatId: string }> {
|
|
14
17
|
const { to, text } = params;
|
|
15
18
|
|
|
16
|
-
const
|
|
19
|
+
const temporaryId = generateTemporaryId();
|
|
20
|
+
// Register before sending: the socket echo can arrive before this HTTP call
|
|
21
|
+
// returns, so the temporaryId (and body fallback) must already be tracked.
|
|
22
|
+
deps.sentTracker?.trackOutbound({ temporaryId, body: text, roomId: to });
|
|
23
|
+
|
|
24
|
+
const result = await sendRoomMessage(deps.apiOpts, { roomId: to, body: text, temporaryId });
|
|
17
25
|
if (!result.ok) {
|
|
18
26
|
throw new Error(`Omadeus send failed: ${result.error}`);
|
|
19
27
|
}
|
|
20
28
|
|
|
29
|
+
if (typeof result.message?.id === "number") {
|
|
30
|
+
deps.sentTracker?.trackId(result.message.id);
|
|
31
|
+
}
|
|
32
|
+
|
|
21
33
|
return {
|
|
22
34
|
channel: "omadeus",
|
|
23
35
|
messageId: String(result.message?.id ?? ""),
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tracks messages this plugin sent so their Jaguar socket echoes can be
|
|
3
|
+
* suppressed, instead of dropping every message authored by the logged-in
|
|
4
|
+
* account.
|
|
5
|
+
*
|
|
6
|
+
* OpenClaw sends as the same Omadeus account it listens on, so each outbound
|
|
7
|
+
* message is broadcast back to us over the socket. We register up to three keys
|
|
8
|
+
* per send:
|
|
9
|
+
*
|
|
10
|
+
* - the client-generated `temporaryId` — known *before* the HTTP round-trip, so
|
|
11
|
+
* it matches even when the socket echo beats the send response (the common
|
|
12
|
+
* race);
|
|
13
|
+
* - the backend message `id` — known once the send response returns;
|
|
14
|
+
* - a normalized copy of the body scoped to its room — a last-resort fallback
|
|
15
|
+
* used only for self-authored echoes that somehow arrive without a
|
|
16
|
+
* recognizable id. Scoping by room prevents the same text sent in one chat
|
|
17
|
+
* from suppressing an identical message in a different chat.
|
|
18
|
+
*
|
|
19
|
+
* `id` and `temporaryId` are kept in separate maps. Entries expire after a
|
|
20
|
+
* short TTL and each map is size-capped, so the tracker cannot grow unbounded.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const DEFAULT_TTL_MS = 2 * 60 * 1000; // 2 minutes — comfortably covers echo latency.
|
|
24
|
+
const DEFAULT_MAX_ENTRIES = 500;
|
|
25
|
+
|
|
26
|
+
export type SentMessageTrackerOptions = {
|
|
27
|
+
ttlMs?: number;
|
|
28
|
+
maxEntries?: number;
|
|
29
|
+
now?: () => number;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function normalizeContent(body: string): string {
|
|
33
|
+
return body.trim();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Normalize a room identity so outbound (`"room:123"`/`"123"`) and the socket
|
|
37
|
+
* echo (numeric `123`) map to the same key. */
|
|
38
|
+
function roomKey(roomId: string | number): string {
|
|
39
|
+
return String(roomId).replace(/^room:/, "").trim();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Build the room-scoped content key, or undefined when the body is empty. */
|
|
43
|
+
function contentKey(roomId: string | number, body: string): string | undefined {
|
|
44
|
+
const normalized = normalizeContent(body);
|
|
45
|
+
if (!normalized) return undefined;
|
|
46
|
+
return `${roomKey(roomId)}\n${normalized}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export class SentMessageTracker {
|
|
50
|
+
private readonly ttlMs: number;
|
|
51
|
+
private readonly maxEntries: number;
|
|
52
|
+
private readonly now: () => number;
|
|
53
|
+
private readonly ids = new Map<number, number>();
|
|
54
|
+
private readonly temporaryIds = new Map<string, number>();
|
|
55
|
+
private readonly contents = new Map<string, number>();
|
|
56
|
+
|
|
57
|
+
constructor(options: SentMessageTrackerOptions = {}) {
|
|
58
|
+
this.ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
|
|
59
|
+
this.maxEntries = options.maxEntries ?? DEFAULT_MAX_ENTRIES;
|
|
60
|
+
this.now = options.now ?? Date.now;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Register a client-generated temporaryId. Call before sending. */
|
|
64
|
+
trackTemporaryId(temporaryId: string): void {
|
|
65
|
+
if (!temporaryId) return;
|
|
66
|
+
this.remember(this.temporaryIds, temporaryId);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Register the backend message id once the send response returns. */
|
|
70
|
+
trackId(id: number): void {
|
|
71
|
+
if (!Number.isFinite(id)) return;
|
|
72
|
+
this.remember(this.ids, id);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Register a message body, scoped to its room, as a fallback match key. */
|
|
76
|
+
trackContent(roomId: string | number, body: string): void {
|
|
77
|
+
const key = contentKey(roomId, body);
|
|
78
|
+
if (!key) return;
|
|
79
|
+
this.remember(this.contents, key);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Convenience: register whichever keys are available for one outbound message. */
|
|
83
|
+
trackOutbound(params: {
|
|
84
|
+
temporaryId?: string;
|
|
85
|
+
id?: number;
|
|
86
|
+
body?: string;
|
|
87
|
+
roomId?: string | number;
|
|
88
|
+
}): void {
|
|
89
|
+
if (params.temporaryId) this.trackTemporaryId(params.temporaryId);
|
|
90
|
+
if (typeof params.id === "number") this.trackId(params.id);
|
|
91
|
+
if (typeof params.body === "string" && params.roomId !== undefined) {
|
|
92
|
+
this.trackContent(params.roomId, params.body);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Returns true when an inbound socket message is an echo of something we sent.
|
|
98
|
+
*
|
|
99
|
+
* `id`/`temporaryId` matches are authoritative. The content fallback only
|
|
100
|
+
* applies to self-authored messages — the only ones that can form a reply
|
|
101
|
+
* loop — and is scoped to the message's room, so a different user repeating
|
|
102
|
+
* our text (or the same text in another room) is never suppressed.
|
|
103
|
+
*/
|
|
104
|
+
isEcho(msg: {
|
|
105
|
+
id?: number;
|
|
106
|
+
temporaryId?: string;
|
|
107
|
+
body?: string;
|
|
108
|
+
roomId?: string | number;
|
|
109
|
+
fromSelf: boolean;
|
|
110
|
+
}): boolean {
|
|
111
|
+
if (typeof msg.id === "number" && this.has(this.ids, msg.id)) return true;
|
|
112
|
+
if (msg.temporaryId && this.has(this.temporaryIds, msg.temporaryId)) return true;
|
|
113
|
+
if (msg.fromSelf && typeof msg.body === "string" && msg.roomId !== undefined) {
|
|
114
|
+
const key = contentKey(msg.roomId, msg.body);
|
|
115
|
+
if (key && this.has(this.contents, key)) return true;
|
|
116
|
+
}
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private remember<K>(map: Map<K, number>, key: K): void {
|
|
121
|
+
// Delete-then-set so re-registered keys move to the end, keeping insertion
|
|
122
|
+
// order aligned with expiry order (the TTL is constant).
|
|
123
|
+
map.delete(key);
|
|
124
|
+
map.set(key, this.now() + this.ttlMs);
|
|
125
|
+
this.prune(map);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private has<K>(map: Map<K, number>, key: K): boolean {
|
|
129
|
+
const expiry = map.get(key);
|
|
130
|
+
if (expiry === undefined) return false;
|
|
131
|
+
if (expiry <= this.now()) {
|
|
132
|
+
map.delete(key);
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private prune<K>(map: Map<K, number>): void {
|
|
139
|
+
const now = this.now();
|
|
140
|
+
for (const [key, expiry] of map) {
|
|
141
|
+
if (expiry <= now) {
|
|
142
|
+
map.delete(key);
|
|
143
|
+
} else {
|
|
144
|
+
// Insertion order matches expiry order, so the first live entry means
|
|
145
|
+
// everything after it is also live.
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
while (map.size > this.maxEntries) {
|
|
150
|
+
const oldest = map.keys().next().value as K | undefined;
|
|
151
|
+
if (oldest === undefined) break;
|
|
152
|
+
map.delete(oldest);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
package/src/setup-core.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ChannelSetupAdapter } from "openclaw/plugin-sdk/setup";
|
|
2
2
|
import type { OpenClawConfig } from "../runtime-api.js";
|
|
3
|
+
import { resolveOmadeusEnvironment } from "./defaults.js";
|
|
3
4
|
|
|
4
5
|
function readSetupStringField(input: Record<string, unknown>, key: string): string | undefined {
|
|
5
6
|
const value = input[key];
|
|
@@ -29,8 +30,8 @@ export const omadeusSetupAdapter: ChannelSetupAdapter = {
|
|
|
29
30
|
},
|
|
30
31
|
applyAccountConfig: ({ cfg, input }) => {
|
|
31
32
|
const rawInput = input as Record<string, unknown>;
|
|
32
|
-
const
|
|
33
|
-
const
|
|
33
|
+
const environmentRaw = readSetupStringField(rawInput, "environment");
|
|
34
|
+
const environment = environmentRaw ? resolveOmadeusEnvironment(environmentRaw) : undefined;
|
|
34
35
|
const email = readSetupStringField(rawInput, "email");
|
|
35
36
|
const password = input.password?.trim() || undefined;
|
|
36
37
|
const organizationId = readSetupNumberField(rawInput, "organizationId");
|
|
@@ -51,8 +52,7 @@ export const omadeusSetupAdapter: ChannelSetupAdapter = {
|
|
|
51
52
|
omadeus: {
|
|
52
53
|
...omadeusPrevious,
|
|
53
54
|
enabled: true,
|
|
54
|
-
...(
|
|
55
|
-
...(maestroUrl ? { maestroUrl } : {}),
|
|
55
|
+
...(environment ? { environment } : {}),
|
|
56
56
|
...(email ? { email } : {}),
|
|
57
57
|
...(password ? { password } : {}),
|
|
58
58
|
...(organizationId ? { organizationId } : {}),
|
package/src/types.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { OmadeusEnvironment } from "./defaults.js";
|
|
2
|
+
|
|
1
3
|
// ---------------------------------------------------------------------------
|
|
2
4
|
// Omadeus config shape (stored under channels.omadeus in OpenClaw config)
|
|
3
5
|
// ---------------------------------------------------------------------------
|
|
@@ -61,13 +63,14 @@ export type OmadeusInboundPolicy = {
|
|
|
61
63
|
|
|
62
64
|
export type OmadeusChannelConfig = {
|
|
63
65
|
enabled?: boolean;
|
|
64
|
-
|
|
65
|
-
maestroUrl?: string;
|
|
66
|
+
environment?: OmadeusEnvironment;
|
|
66
67
|
email?: string;
|
|
67
68
|
password?: string;
|
|
68
69
|
organizationId?: number;
|
|
69
70
|
/** Cached Omadeus session JWT obtained during onboarding/startup. */
|
|
70
71
|
sessionToken?: string;
|
|
72
|
+
/** Environment the cached sessionToken was minted under (must match `environment`). */
|
|
73
|
+
sessionTokenEnvironment?: OmadeusEnvironment;
|
|
71
74
|
/** Jaguar chat ingress allowlists and mention rules. */
|
|
72
75
|
inbound?: OmadeusInboundPolicy;
|
|
73
76
|
};
|
|
@@ -77,6 +80,7 @@ export type ResolvedOmadeusAccount = {
|
|
|
77
80
|
name?: string;
|
|
78
81
|
enabled: boolean;
|
|
79
82
|
config: OmadeusChannelConfig;
|
|
83
|
+
environment: OmadeusEnvironment;
|
|
80
84
|
casUrl: string;
|
|
81
85
|
maestroUrl: string;
|
|
82
86
|
email: string;
|