@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
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
//#region src/sent-message-tracker.ts
|
|
2
|
+
/**
|
|
3
|
+
* Tracks messages this plugin sent so their Jaguar socket echoes can be
|
|
4
|
+
* suppressed, instead of dropping every message authored by the logged-in
|
|
5
|
+
* account.
|
|
6
|
+
*
|
|
7
|
+
* OpenClaw sends as the same Omadeus account it listens on, so each outbound
|
|
8
|
+
* message is broadcast back to us over the socket. We register up to three keys
|
|
9
|
+
* per send:
|
|
10
|
+
*
|
|
11
|
+
* - the client-generated `temporaryId` — known *before* the HTTP round-trip, so
|
|
12
|
+
* it matches even when the socket echo beats the send response (the common
|
|
13
|
+
* race);
|
|
14
|
+
* - the backend message `id` — known once the send response returns;
|
|
15
|
+
* - a normalized copy of the body scoped to its room — a last-resort fallback
|
|
16
|
+
* used only for self-authored echoes that somehow arrive without a
|
|
17
|
+
* recognizable id. Scoping by room prevents the same text sent in one chat
|
|
18
|
+
* from suppressing an identical message in a different chat.
|
|
19
|
+
*
|
|
20
|
+
* `id` and `temporaryId` are kept in separate maps. Entries expire after a
|
|
21
|
+
* short TTL and each map is size-capped, so the tracker cannot grow unbounded.
|
|
22
|
+
*/
|
|
23
|
+
const DEFAULT_TTL_MS = 120 * 1e3;
|
|
24
|
+
const DEFAULT_MAX_ENTRIES = 500;
|
|
25
|
+
function normalizeContent(body) {
|
|
26
|
+
return body.trim();
|
|
27
|
+
}
|
|
28
|
+
/** Normalize a room identity so outbound (`"room:123"`/`"123"`) and the socket
|
|
29
|
+
* echo (numeric `123`) map to the same key. */
|
|
30
|
+
function roomKey(roomId) {
|
|
31
|
+
return String(roomId).replace(/^room:/, "").trim();
|
|
32
|
+
}
|
|
33
|
+
/** Build the room-scoped content key, or undefined when the body is empty. */
|
|
34
|
+
function contentKey(roomId, body) {
|
|
35
|
+
const normalized = normalizeContent(body);
|
|
36
|
+
if (!normalized) return void 0;
|
|
37
|
+
return `${roomKey(roomId)}\n${normalized}`;
|
|
38
|
+
}
|
|
39
|
+
var SentMessageTracker = class {
|
|
40
|
+
ttlMs;
|
|
41
|
+
maxEntries;
|
|
42
|
+
now;
|
|
43
|
+
ids = /* @__PURE__ */ new Map();
|
|
44
|
+
temporaryIds = /* @__PURE__ */ new Map();
|
|
45
|
+
contents = /* @__PURE__ */ new Map();
|
|
46
|
+
constructor(options = {}) {
|
|
47
|
+
this.ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
|
|
48
|
+
this.maxEntries = options.maxEntries ?? DEFAULT_MAX_ENTRIES;
|
|
49
|
+
this.now = options.now ?? Date.now;
|
|
50
|
+
}
|
|
51
|
+
/** Register a client-generated temporaryId. Call before sending. */
|
|
52
|
+
trackTemporaryId(temporaryId) {
|
|
53
|
+
if (!temporaryId) return;
|
|
54
|
+
this.remember(this.temporaryIds, temporaryId);
|
|
55
|
+
}
|
|
56
|
+
/** Register the backend message id once the send response returns. */
|
|
57
|
+
trackId(id) {
|
|
58
|
+
if (!Number.isFinite(id)) return;
|
|
59
|
+
this.remember(this.ids, id);
|
|
60
|
+
}
|
|
61
|
+
/** Register a message body, scoped to its room, as a fallback match key. */
|
|
62
|
+
trackContent(roomId, body) {
|
|
63
|
+
const key = contentKey(roomId, body);
|
|
64
|
+
if (!key) return;
|
|
65
|
+
this.remember(this.contents, key);
|
|
66
|
+
}
|
|
67
|
+
/** Convenience: register whichever keys are available for one outbound message. */
|
|
68
|
+
trackOutbound(params) {
|
|
69
|
+
if (params.temporaryId) this.trackTemporaryId(params.temporaryId);
|
|
70
|
+
if (typeof params.id === "number") this.trackId(params.id);
|
|
71
|
+
if (typeof params.body === "string" && params.roomId !== void 0) this.trackContent(params.roomId, params.body);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Returns true when an inbound socket message is an echo of something we sent.
|
|
75
|
+
*
|
|
76
|
+
* `id`/`temporaryId` matches are authoritative. The content fallback only
|
|
77
|
+
* applies to self-authored messages — the only ones that can form a reply
|
|
78
|
+
* loop — and is scoped to the message's room, so a different user repeating
|
|
79
|
+
* our text (or the same text in another room) is never suppressed.
|
|
80
|
+
*/
|
|
81
|
+
isEcho(msg) {
|
|
82
|
+
if (typeof msg.id === "number" && this.has(this.ids, msg.id)) return true;
|
|
83
|
+
if (msg.temporaryId && this.has(this.temporaryIds, msg.temporaryId)) return true;
|
|
84
|
+
if (msg.fromSelf && typeof msg.body === "string" && msg.roomId !== void 0) {
|
|
85
|
+
const key = contentKey(msg.roomId, msg.body);
|
|
86
|
+
if (key && this.has(this.contents, key)) return true;
|
|
87
|
+
}
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
remember(map, key) {
|
|
91
|
+
map.delete(key);
|
|
92
|
+
map.set(key, this.now() + this.ttlMs);
|
|
93
|
+
this.prune(map);
|
|
94
|
+
}
|
|
95
|
+
has(map, key) {
|
|
96
|
+
const expiry = map.get(key);
|
|
97
|
+
if (expiry === void 0) return false;
|
|
98
|
+
if (expiry <= this.now()) {
|
|
99
|
+
map.delete(key);
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
prune(map) {
|
|
105
|
+
const now = this.now();
|
|
106
|
+
for (const [key, expiry] of map) if (expiry <= now) map.delete(key);
|
|
107
|
+
else break;
|
|
108
|
+
while (map.size > this.maxEntries) {
|
|
109
|
+
const oldest = map.keys().next().value;
|
|
110
|
+
if (oldest === void 0) break;
|
|
111
|
+
map.delete(oldest);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
//#endregion
|
|
116
|
+
export { SentMessageTracker };
|
package/dist/src/setup-core.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveOmadeusEnvironment } from "./defaults.js";
|
|
1
2
|
//#region src/setup-core.ts
|
|
2
3
|
function readSetupStringField(input, key) {
|
|
3
4
|
const value = input[key];
|
|
@@ -18,8 +19,8 @@ const omadeusSetupAdapter = {
|
|
|
18
19
|
},
|
|
19
20
|
applyAccountConfig: ({ cfg, input }) => {
|
|
20
21
|
const rawInput = input;
|
|
21
|
-
const
|
|
22
|
-
const
|
|
22
|
+
const environmentRaw = readSetupStringField(rawInput, "environment");
|
|
23
|
+
const environment = environmentRaw ? resolveOmadeusEnvironment(environmentRaw) : void 0;
|
|
23
24
|
const email = readSetupStringField(rawInput, "email");
|
|
24
25
|
const password = input.password?.trim() || void 0;
|
|
25
26
|
const organizationId = readSetupNumberField(rawInput, "organizationId");
|
|
@@ -32,8 +33,7 @@ const omadeusSetupAdapter = {
|
|
|
32
33
|
omadeus: {
|
|
33
34
|
...omadeusPrevious,
|
|
34
35
|
enabled: true,
|
|
35
|
-
...
|
|
36
|
-
...maestroUrl ? { maestroUrl } : {},
|
|
36
|
+
...environment ? { environment } : {},
|
|
37
37
|
...email ? { email } : {},
|
|
38
38
|
...password ? { password } : {},
|
|
39
39
|
...organizationId ? { organizationId } : {}
|
package/openclaw.plugin.json
CHANGED
|
@@ -6,12 +6,18 @@
|
|
|
6
6
|
"additionalProperties": false,
|
|
7
7
|
"properties": {
|
|
8
8
|
"enabled": { "type": "boolean" },
|
|
9
|
-
"
|
|
10
|
-
|
|
9
|
+
"environment": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"enum": ["production", "staging", "dev"]
|
|
12
|
+
},
|
|
11
13
|
"email": { "type": "string" },
|
|
12
14
|
"password": { "type": "string" },
|
|
13
15
|
"organizationId": { "type": "number" },
|
|
14
16
|
"sessionToken": { "type": "string" },
|
|
17
|
+
"sessionTokenEnvironment": {
|
|
18
|
+
"type": "string",
|
|
19
|
+
"enum": ["production", "staging", "dev"]
|
|
20
|
+
},
|
|
15
21
|
"inbound": {
|
|
16
22
|
"type": "object",
|
|
17
23
|
"additionalProperties": false,
|
|
@@ -92,12 +98,18 @@
|
|
|
92
98
|
"additionalProperties": false,
|
|
93
99
|
"properties": {
|
|
94
100
|
"enabled": { "type": "boolean" },
|
|
95
|
-
"
|
|
96
|
-
|
|
101
|
+
"environment": {
|
|
102
|
+
"type": "string",
|
|
103
|
+
"enum": ["production", "staging", "dev"]
|
|
104
|
+
},
|
|
97
105
|
"email": { "type": "string" },
|
|
98
106
|
"password": { "type": "string" },
|
|
99
107
|
"organizationId": { "type": "number" },
|
|
100
108
|
"sessionToken": { "type": "string" },
|
|
109
|
+
"sessionTokenEnvironment": {
|
|
110
|
+
"type": "string",
|
|
111
|
+
"enum": ["production", "staging", "dev"]
|
|
112
|
+
},
|
|
101
113
|
"inbound": {
|
|
102
114
|
"type": "object",
|
|
103
115
|
"additionalProperties": false,
|
|
@@ -172,13 +184,8 @@
|
|
|
172
184
|
}
|
|
173
185
|
},
|
|
174
186
|
"uiHints": {
|
|
175
|
-
"
|
|
176
|
-
"label": "
|
|
177
|
-
"placeholder": "https://dev1-cas.rouztech.com"
|
|
178
|
-
},
|
|
179
|
-
"maestroUrl": {
|
|
180
|
-
"label": "Maestro URL",
|
|
181
|
-
"placeholder": "https://dev3-maestro.rouztech.com"
|
|
187
|
+
"environment": {
|
|
188
|
+
"label": "Environment"
|
|
182
189
|
},
|
|
183
190
|
"email": {
|
|
184
191
|
"label": "Email"
|
|
@@ -195,6 +202,10 @@
|
|
|
195
202
|
"sensitive": true,
|
|
196
203
|
"advanced": true
|
|
197
204
|
},
|
|
205
|
+
"sessionTokenEnvironment": {
|
|
206
|
+
"label": "Session token environment",
|
|
207
|
+
"advanced": true
|
|
208
|
+
},
|
|
198
209
|
"inbound": {
|
|
199
210
|
"label": "Inbound policy (Jaguar chat)",
|
|
200
211
|
"advanced": true
|
package/package.json
CHANGED
package/src/api/message.api.ts
CHANGED
|
@@ -19,14 +19,14 @@ async function readJsonOrEmpty(res: Response): Promise<unknown> {
|
|
|
19
19
|
|
|
20
20
|
export async function sendRoomMessage(
|
|
21
21
|
opts: OmadeusApiOptions,
|
|
22
|
-
params: { roomId: number | string; body: string },
|
|
22
|
+
params: { roomId: number | string; body: string; temporaryId?: string },
|
|
23
23
|
): Promise<{ ok: boolean; message?: OmadeusMessage; error?: string }> {
|
|
24
24
|
try {
|
|
25
25
|
const res = await jaguarFetch(opts, `/rooms/${params.roomId}/messages`, {
|
|
26
26
|
method: "SEND",
|
|
27
27
|
body: JSON.stringify({
|
|
28
28
|
body: params.body,
|
|
29
|
-
temporaryId: generateTemporaryId(),
|
|
29
|
+
temporaryId: params.temporaryId ?? generateTemporaryId(),
|
|
30
30
|
links: "[]",
|
|
31
31
|
}),
|
|
32
32
|
});
|
|
@@ -68,6 +68,8 @@ export async function seeMessage(
|
|
|
68
68
|
): Promise<OmadeusMessage> {
|
|
69
69
|
const res = await jaguarFetch(opts, `/messages/${params.messageId}`, {
|
|
70
70
|
method: "SEE",
|
|
71
|
+
// The server requires a Content-Length header; an empty JSON body supplies one.
|
|
72
|
+
body: "{}",
|
|
71
73
|
});
|
|
72
74
|
if (!res.ok) {
|
|
73
75
|
const text = await res.text().catch(() => "");
|
package/src/channel.ts
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
type OmadeusNuggetPriority,
|
|
28
28
|
} from "./api/nugget.api.js";
|
|
29
29
|
import { addMessageReaction, deleteMessage, editMessage } from "./api/message.api.js";
|
|
30
|
+
import { generateTemporaryId } from "./utils/http.util.js";
|
|
30
31
|
import {
|
|
31
32
|
getOmadeusChannelConfig,
|
|
32
33
|
listOmadeusAccountIds,
|
|
@@ -38,6 +39,7 @@ import { createOmadeusMessageHandler } from "./message-handler.js";
|
|
|
38
39
|
import { parseTaskChannelTargetIntent } from "./nugget-lookup.js";
|
|
39
40
|
import { sendOmadeusMessage, type OutboundDeps } from "./outbound.js";
|
|
40
41
|
import { getOmadeusRuntime } from "./runtime.js";
|
|
42
|
+
import { SentMessageTracker } from "./sent-message-tracker.js";
|
|
41
43
|
import { omadeusSetupAdapter } from "./setup-core.js";
|
|
42
44
|
import { omadeusSetupWizard } from "./setup-surface.js";
|
|
43
45
|
import { createDolphinSocketClient, type DolphinSocketClient } from "./socket/dolphin.socket.js";
|
|
@@ -51,18 +53,22 @@ const gatewayState: {
|
|
|
51
53
|
tokenManager: OmadeusTokenManager | null;
|
|
52
54
|
dolphin: DolphinSocketClient | null;
|
|
53
55
|
jaguar: JaguarSocketClient | null;
|
|
54
|
-
|
|
56
|
+
sentTracker: SentMessageTracker | null;
|
|
57
|
+
} = { tokenManager: null, dolphin: null, jaguar: null, sentTracker: null };
|
|
55
58
|
|
|
56
59
|
const isUnconfigured = (account: Account) => account.credentialSource === "none";
|
|
57
60
|
|
|
58
61
|
let lastPersistedToken: string | null = null;
|
|
59
62
|
|
|
60
|
-
async function persistSessionToken(
|
|
63
|
+
async function persistSessionToken(
|
|
64
|
+
token: string,
|
|
65
|
+
environment: Account["environment"],
|
|
66
|
+
): Promise<void> {
|
|
61
67
|
if (lastPersistedToken === token) return;
|
|
62
68
|
const runtime = getOmadeusRuntime();
|
|
63
69
|
const cfg = runtime.config.current() as OpenClawConfig;
|
|
64
70
|
const section = getOmadeusChannelConfig(cfg) ?? {};
|
|
65
|
-
if (section.sessionToken === token) {
|
|
71
|
+
if (section.sessionToken === token && section.sessionTokenEnvironment === environment) {
|
|
66
72
|
lastPersistedToken = token;
|
|
67
73
|
return;
|
|
68
74
|
}
|
|
@@ -74,6 +80,7 @@ async function persistSessionToken(token: string): Promise<void> {
|
|
|
74
80
|
omadeus: {
|
|
75
81
|
...(getOmadeusChannelConfig(draft) ?? {}),
|
|
76
82
|
sessionToken: token,
|
|
83
|
+
sessionTokenEnvironment: environment,
|
|
77
84
|
},
|
|
78
85
|
};
|
|
79
86
|
},
|
|
@@ -103,12 +110,12 @@ const omadeusConfigAdapter = createTopLevelChannelConfigAdapter<Account>({
|
|
|
103
110
|
defaultAccountId: resolveDefaultOmadeusAccountId,
|
|
104
111
|
deleteMode: "clear-fields",
|
|
105
112
|
clearBaseFields: [
|
|
106
|
-
"
|
|
107
|
-
"maestroUrl",
|
|
113
|
+
"environment",
|
|
108
114
|
"email",
|
|
109
115
|
"password",
|
|
110
116
|
"organizationId",
|
|
111
117
|
"sessionToken",
|
|
118
|
+
"sessionTokenEnvironment",
|
|
112
119
|
"inbound",
|
|
113
120
|
],
|
|
114
121
|
// Keep adapter contract satisfied even though Omadeus no longer uses DM allowlists.
|
|
@@ -315,7 +322,13 @@ export const omadeusPlugin: ChannelPlugin<Account> = {
|
|
|
315
322
|
);
|
|
316
323
|
}
|
|
317
324
|
try {
|
|
318
|
-
|
|
325
|
+
const temporaryId = generateTemporaryId();
|
|
326
|
+
// Track before editing so the edit's socket echo is recognized as ours.
|
|
327
|
+
gatewayState.sentTracker?.trackOutbound({ temporaryId, body });
|
|
328
|
+
const edited = await editMessage(apiOpts(), { messageId, body, temporaryId });
|
|
329
|
+
if (typeof edited?.id === "number") {
|
|
330
|
+
gatewayState.sentTracker?.trackId(edited.id);
|
|
331
|
+
}
|
|
319
332
|
} catch (err) {
|
|
320
333
|
const msg = err instanceof Error ? err.message : String(err);
|
|
321
334
|
return actionError(msg);
|
|
@@ -462,6 +475,7 @@ export const omadeusPlugin: ChannelPlugin<Account> = {
|
|
|
462
475
|
tokenManager: gatewayState.tokenManager,
|
|
463
476
|
},
|
|
464
477
|
jaguarSocket: gatewayState.jaguar,
|
|
478
|
+
sentTracker: gatewayState.sentTracker ?? undefined,
|
|
465
479
|
};
|
|
466
480
|
return await sendOmadeusMessage(deps, { to, text });
|
|
467
481
|
},
|
|
@@ -561,7 +575,7 @@ export const omadeusPlugin: ChannelPlugin<Account> = {
|
|
|
561
575
|
initialToken: account.sessionToken,
|
|
562
576
|
onRefresh: (token) => {
|
|
563
577
|
log.info("[omadeus] token refreshed");
|
|
564
|
-
void persistSessionToken(token).catch((err) =>
|
|
578
|
+
void persistSessionToken(token, account.environment).catch((err) =>
|
|
565
579
|
log.warn(`[omadeus] failed to persist session token: ${String(err)}`),
|
|
566
580
|
);
|
|
567
581
|
},
|
|
@@ -585,9 +599,13 @@ export const omadeusPlugin: ChannelPlugin<Account> = {
|
|
|
585
599
|
|
|
586
600
|
const selfReferenceId = tokenManager.getPayload().referenceId;
|
|
587
601
|
|
|
602
|
+
const sentTracker = new SentMessageTracker();
|
|
603
|
+
gatewayState.sentTracker = sentTracker;
|
|
604
|
+
|
|
588
605
|
const outboundDeps: OutboundDeps = {
|
|
589
606
|
apiOpts: { maestroUrl: account.maestroUrl, tokenManager },
|
|
590
607
|
jaguarSocket: null as unknown as JaguarSocketClient,
|
|
608
|
+
sentTracker,
|
|
591
609
|
};
|
|
592
610
|
|
|
593
611
|
const handleMessage = createOmadeusMessageHandler({
|
|
@@ -609,6 +627,23 @@ export const omadeusPlugin: ChannelPlugin<Account> = {
|
|
|
609
627
|
: `${msg.subscribableKind}/${msg.roomName ?? msg.roomId} from ${msg.senderReferenceId}`;
|
|
610
628
|
log.info(`[jaguar] ${label}: ${msg.body.slice(0, 80)}`);
|
|
611
629
|
|
|
630
|
+
// Suppress echoes of messages we sent (we send as the logged-in
|
|
631
|
+
// account, so our own messages come back over the socket). This
|
|
632
|
+
// replaces the old "drop everything from self" rule, letting the
|
|
633
|
+
// logged-in user message their own OpenClaw.
|
|
634
|
+
if (
|
|
635
|
+
sentTracker.isEcho({
|
|
636
|
+
id: msg.id,
|
|
637
|
+
temporaryId: msg.temporaryId,
|
|
638
|
+
body: msg.body,
|
|
639
|
+
roomId: msg.roomId,
|
|
640
|
+
fromSelf: msg.senderReferenceId === selfReferenceId,
|
|
641
|
+
})
|
|
642
|
+
) {
|
|
643
|
+
log.debug?.(`[jaguar] suppressed self-echo id=${msg.id}`);
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
|
|
612
647
|
const inbound = parseJaguarMessage(msg, { selfReferenceId }, log);
|
|
613
648
|
if (inbound) {
|
|
614
649
|
log.info(
|
|
@@ -684,6 +719,7 @@ export const omadeusPlugin: ChannelPlugin<Account> = {
|
|
|
684
719
|
gatewayState.tokenManager = null;
|
|
685
720
|
gatewayState.jaguar = null;
|
|
686
721
|
gatewayState.dolphin = null;
|
|
722
|
+
gatewayState.sentTracker = null;
|
|
687
723
|
lastPersistedToken = null;
|
|
688
724
|
ctx.setStatus({
|
|
689
725
|
accountId: account.accountId,
|
package/src/config.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { DEFAULT_ACCOUNT_ID, type OpenClawConfig } from "../runtime-api.js";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
getOmadeusEnvironmentUrls,
|
|
4
|
+
resolveOmadeusEnvironment,
|
|
5
|
+
} from "./defaults.js";
|
|
3
6
|
import type { OmadeusChannelConfig, ResolvedOmadeusAccount } from "./types.js";
|
|
4
7
|
|
|
5
8
|
export function getOmadeusChannelConfig(cfg: OpenClawConfig): OmadeusChannelConfig | undefined {
|
|
@@ -24,11 +27,17 @@ export function resolveOmadeusAccount(params: {
|
|
|
24
27
|
}): ResolvedOmadeusAccount {
|
|
25
28
|
const { cfg } = params;
|
|
26
29
|
const section = getOmadeusChannelConfig(cfg) ?? {};
|
|
30
|
+
const environment = resolveOmadeusEnvironment(section.environment);
|
|
31
|
+
const { casUrl, maestroUrl } = getOmadeusEnvironmentUrls(environment);
|
|
27
32
|
const envCredentials = resolveOmadeusEnvCredentials();
|
|
28
33
|
const email = section.email?.trim() || envCredentials?.email || "";
|
|
29
34
|
const password = section.password?.trim() || envCredentials?.password || "";
|
|
30
35
|
const orgId = section.organizationId ?? envCredentials?.organizationId;
|
|
31
|
-
const
|
|
36
|
+
const rawSessionToken = section.sessionToken?.trim() ?? "";
|
|
37
|
+
const sessionTokenEnvironment = resolveOmadeusEnvironment(section.sessionTokenEnvironment);
|
|
38
|
+
const sessionTokenValid =
|
|
39
|
+
Boolean(rawSessionToken) && sessionTokenEnvironment === environment;
|
|
40
|
+
const sessionToken = sessionTokenValid ? rawSessionToken : "";
|
|
32
41
|
const hasCredentials = Boolean(email && password && orgId);
|
|
33
42
|
const hasSessionToken = Boolean(sessionToken);
|
|
34
43
|
const hasConfigCredentials = Boolean(
|
|
@@ -47,8 +56,9 @@ export function resolveOmadeusAccount(params: {
|
|
|
47
56
|
name: "Omadeus",
|
|
48
57
|
enabled: section.enabled !== false,
|
|
49
58
|
config: section,
|
|
50
|
-
|
|
51
|
-
|
|
59
|
+
environment,
|
|
60
|
+
casUrl,
|
|
61
|
+
maestroUrl,
|
|
52
62
|
email,
|
|
53
63
|
password,
|
|
54
64
|
organizationId: orgId ?? 0,
|
package/src/defaults.ts
CHANGED
|
@@ -1,2 +1,44 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
1
|
+
export type OmadeusEnvironment = "production" | "staging" | "dev";
|
|
2
|
+
|
|
3
|
+
export const OMADEUS_DEFAULT_ENVIRONMENT: OmadeusEnvironment = "dev";
|
|
4
|
+
|
|
5
|
+
export type OmadeusEnvironmentConfig = {
|
|
6
|
+
label: string;
|
|
7
|
+
casUrl: string;
|
|
8
|
+
maestroUrl: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const OMADEUS_ENVIRONMENTS: Record<OmadeusEnvironment, OmadeusEnvironmentConfig> = {
|
|
12
|
+
production: {
|
|
13
|
+
label: "Production",
|
|
14
|
+
casUrl: "https://xas.xeba.tech",
|
|
15
|
+
maestroUrl: "https://maestro.xeba.tech",
|
|
16
|
+
},
|
|
17
|
+
staging: {
|
|
18
|
+
label: "Staging",
|
|
19
|
+
casUrl: "https://staging-xas.xeba.tech",
|
|
20
|
+
maestroUrl: "https://staging.xeba.tech",
|
|
21
|
+
},
|
|
22
|
+
dev: {
|
|
23
|
+
label: "Dev",
|
|
24
|
+
casUrl: "https://dev1-cas.rouztech.com",
|
|
25
|
+
maestroUrl: "https://dev1-maestro.rouztech.com",
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const OMADEUS_ENVIRONMENT_SET = new Set<string>(Object.keys(OMADEUS_ENVIRONMENTS));
|
|
30
|
+
|
|
31
|
+
export function resolveOmadeusEnvironment(value: unknown): OmadeusEnvironment {
|
|
32
|
+
if (typeof value === "string" && OMADEUS_ENVIRONMENT_SET.has(value)) {
|
|
33
|
+
return value as OmadeusEnvironment;
|
|
34
|
+
}
|
|
35
|
+
return OMADEUS_DEFAULT_ENVIRONMENT;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function getOmadeusEnvironmentUrls(env: OmadeusEnvironment): {
|
|
39
|
+
casUrl: string;
|
|
40
|
+
maestroUrl: string;
|
|
41
|
+
} {
|
|
42
|
+
const config = OMADEUS_ENVIRONMENTS[env];
|
|
43
|
+
return { casUrl: config.casUrl, maestroUrl: config.maestroUrl };
|
|
44
|
+
}
|
package/src/inbound-policy.ts
CHANGED
|
@@ -49,7 +49,15 @@ function surfaceForKind(kind: OmadeusSubscribableKind): "direct" | "channel" | "
|
|
|
49
49
|
return "entity";
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
function senderAllowed(
|
|
52
|
+
function senderAllowed(
|
|
53
|
+
allowed: number[] | undefined,
|
|
54
|
+
fromReferenceId: number,
|
|
55
|
+
selfReferenceId: number,
|
|
56
|
+
): boolean {
|
|
57
|
+
// The logged-in user can always reach their own instance, regardless of the
|
|
58
|
+
// configured allowlist. Their own echoes are filtered earlier by the
|
|
59
|
+
// SentMessageTracker, so this cannot create a reply loop.
|
|
60
|
+
if (fromReferenceId === selfReferenceId) return true;
|
|
53
61
|
if (!allowed || allowed.length === 0) return true;
|
|
54
62
|
return allowed.includes(fromReferenceId);
|
|
55
63
|
}
|
|
@@ -115,7 +123,11 @@ function mentionRequired(params: {
|
|
|
115
123
|
|
|
116
124
|
/**
|
|
117
125
|
* Evaluate whether a normalized Jaguar inbound should be dispatched to OpenClaw.
|
|
118
|
-
*
|
|
126
|
+
*
|
|
127
|
+
* The logged-in user (`selfReferenceId`) is always treated as an allowed sender
|
|
128
|
+
* so they can message their own OpenClaw even if the stored allowlist predates
|
|
129
|
+
* them. Self-authored *echoes* (the reply loop) are filtered earlier, at socket
|
|
130
|
+
* ingestion, by the {@link SentMessageTracker} — not here.
|
|
119
131
|
*/
|
|
120
132
|
export function evaluateOmadeusInboundPolicy(params: {
|
|
121
133
|
inbound: OmadeusInboundMessage;
|
|
@@ -124,14 +136,6 @@ export function evaluateOmadeusInboundPolicy(params: {
|
|
|
124
136
|
}): InboundPolicyDecision {
|
|
125
137
|
const { inbound, omadeusCfg, selfReferenceId } = params;
|
|
126
138
|
|
|
127
|
-
if (inbound.fromReferenceId === selfReferenceId) {
|
|
128
|
-
return {
|
|
129
|
-
allow: false,
|
|
130
|
-
reason: "self_message",
|
|
131
|
-
details: { fromReferenceId: inbound.fromReferenceId, selfReferenceId },
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
|
|
135
139
|
const policy = mergePolicy(omadeusCfg);
|
|
136
140
|
const surface = surfaceForKind(inbound.subscribableKind);
|
|
137
141
|
|
|
@@ -139,7 +143,9 @@ export function evaluateOmadeusInboundPolicy(params: {
|
|
|
139
143
|
if (!policy.direct.enabled) {
|
|
140
144
|
return { allow: false, reason: "direct_disabled", details: { surface } };
|
|
141
145
|
}
|
|
142
|
-
if (
|
|
146
|
+
if (
|
|
147
|
+
!senderAllowed(policy.direct.allowedSenderReferenceIds, inbound.fromReferenceId, selfReferenceId)
|
|
148
|
+
) {
|
|
143
149
|
return {
|
|
144
150
|
allow: false,
|
|
145
151
|
reason: "direct_sender_not_allowed",
|
|
@@ -157,7 +163,9 @@ export function evaluateOmadeusInboundPolicy(params: {
|
|
|
157
163
|
if (!policy.channels.enabled) {
|
|
158
164
|
return { allow: false, reason: "channels_disabled", details: { surface } };
|
|
159
165
|
}
|
|
160
|
-
if (
|
|
166
|
+
if (
|
|
167
|
+
!senderAllowed(policy.channels.allowedSenderReferenceIds, inbound.fromReferenceId, selfReferenceId)
|
|
168
|
+
) {
|
|
161
169
|
return {
|
|
162
170
|
allow: false,
|
|
163
171
|
reason: "channel_sender_not_allowed",
|
|
@@ -171,6 +179,7 @@ export function evaluateOmadeusInboundPolicy(params: {
|
|
|
171
179
|
allowedChannelViewIds: policy.channels.allowedChannelViewIds,
|
|
172
180
|
});
|
|
173
181
|
const senderInList =
|
|
182
|
+
inbound.fromReferenceId === selfReferenceId ||
|
|
174
183
|
!policy.channels.allowedSenderReferenceIds ||
|
|
175
184
|
policy.channels.allowedSenderReferenceIds.length === 0 ||
|
|
176
185
|
policy.channels.allowedSenderReferenceIds.includes(inbound.fromReferenceId);
|
|
@@ -208,7 +217,9 @@ export function evaluateOmadeusInboundPolicy(params: {
|
|
|
208
217
|
details: { kind: inbound.subscribableKind, allowedKinds: policy.entities.allowedKinds },
|
|
209
218
|
};
|
|
210
219
|
}
|
|
211
|
-
if (
|
|
220
|
+
if (
|
|
221
|
+
!senderAllowed(policy.entities.allowedSenderReferenceIds, inbound.fromReferenceId, selfReferenceId)
|
|
222
|
+
) {
|
|
212
223
|
return {
|
|
213
224
|
allow: false,
|
|
214
225
|
reason: "entity_sender_not_allowed",
|
package/src/message-handler.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
type RuntimeEnv,
|
|
7
7
|
} from "../runtime-api.js";
|
|
8
8
|
import { createNugget, findNuggetByTaskChannelRoom, resolveTaskChannelRoomId, searchNuggetByNumber } from "./api/nugget.api.js";
|
|
9
|
+
import { seeMessage } from "./api/message.api.js";
|
|
9
10
|
import {
|
|
10
11
|
appendNuggetContextForTaskOrNuggetRoom,
|
|
11
12
|
appendNuggetLookupContextForAgent,
|
|
@@ -68,7 +69,7 @@ export type OmadeusMessageHandlerDeps = {
|
|
|
68
69
|
runtime: RuntimeEnv;
|
|
69
70
|
log: Log;
|
|
70
71
|
outboundDeps: OutboundDeps;
|
|
71
|
-
/** Authenticated Omadeus user
|
|
72
|
+
/** Authenticated Omadeus user reference id. */
|
|
72
73
|
selfReferenceId: number;
|
|
73
74
|
};
|
|
74
75
|
|
|
@@ -82,7 +83,25 @@ export function createOmadeusMessageHandler(deps: OmadeusMessageHandlerDeps) {
|
|
|
82
83
|
channel: "omadeus",
|
|
83
84
|
});
|
|
84
85
|
|
|
85
|
-
|
|
86
|
+
/** Mark inbound messages as seen in Omadeus (fire-and-forget). */
|
|
87
|
+
const markMessagesSeen = (messageIds: number[]) => {
|
|
88
|
+
for (const messageId of messageIds) {
|
|
89
|
+
if (!Number.isFinite(messageId)) continue;
|
|
90
|
+
log.info(`omadeus: marking message ${messageId} seen`);
|
|
91
|
+
seeMessage(outboundDeps.apiOpts, { messageId })
|
|
92
|
+
.then(() => log.debug?.(`omadeus: marked message ${messageId} seen`))
|
|
93
|
+
.catch((err) => {
|
|
94
|
+
log.warn(
|
|
95
|
+
`omadeus: failed to mark message ${messageId} seen: ${err instanceof Error ? err.message : String(err)}`,
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const handleMessageNow = async (
|
|
102
|
+
inbound: OmadeusInboundMessage,
|
|
103
|
+
ackMessageIds: number[] = [inbound.messageId],
|
|
104
|
+
) => {
|
|
86
105
|
const isDirectMessage = inbound.subscribableKind === "direct";
|
|
87
106
|
const senderId = String(inbound.fromReferenceId);
|
|
88
107
|
const senderName = inbound.from;
|
|
@@ -132,6 +151,12 @@ export function createOmadeusMessageHandler(deps: OmadeusMessageHandlerDeps) {
|
|
|
132
151
|
return;
|
|
133
152
|
}
|
|
134
153
|
|
|
154
|
+
// Committed to dispatching to the agent — mark the source message(s) seen.
|
|
155
|
+
// Never mark our own messages seen (the user can DM their own instance).
|
|
156
|
+
if (inbound.fromReferenceId !== selfReferenceId) {
|
|
157
|
+
markMessagesSeen(ackMessageIds);
|
|
158
|
+
}
|
|
159
|
+
|
|
135
160
|
let bodyForAgent = rawBody;
|
|
136
161
|
const createIntent = parseChannelTaskCreateIntent(rawBody);
|
|
137
162
|
if (createIntent) {
|
|
@@ -386,11 +411,14 @@ export function createOmadeusMessageHandler(deps: OmadeusMessageHandlerDeps) {
|
|
|
386
411
|
.join("\n");
|
|
387
412
|
if (!combinedContent.trim()) return;
|
|
388
413
|
|
|
389
|
-
await handleMessageNow(
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
414
|
+
await handleMessageNow(
|
|
415
|
+
{
|
|
416
|
+
...last,
|
|
417
|
+
content: combinedContent,
|
|
418
|
+
isMention: entries.some((e) => e.isMention),
|
|
419
|
+
},
|
|
420
|
+
entries.map((e) => e.messageId),
|
|
421
|
+
);
|
|
394
422
|
},
|
|
395
423
|
onError: (err) => {
|
|
396
424
|
runtime.error?.(`omadeus debounce flush failed: ${String(err)}`);
|