@brantrusnak/openclaw-omadeus 1.0.2 → 1.0.4
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 +15 -61
- package/dist/_virtual/_rolldown/runtime.js +4 -0
- package/dist/api.js +5 -0
- package/dist/index.js +14 -0
- package/dist/runtime-api.js +15 -0
- package/dist/setup-entry.js +7 -0
- package/dist/src/allowed-reaction-emojis.js +21 -0
- package/dist/src/api/auth.api.js +118 -0
- package/dist/src/api/channel.api.js +23 -0
- package/dist/src/api/message.api.js +76 -0
- package/dist/src/api/nugget.api.js +127 -0
- package/dist/src/auth.js +30 -0
- package/dist/src/channel.js +626 -0
- package/dist/src/config.js +52 -0
- package/dist/src/defaults.js +5 -0
- package/dist/src/inbound-policy.js +205 -0
- package/dist/src/inbound.js +97 -0
- package/dist/src/member-resolve.js +53 -0
- package/dist/src/message-handler.js +262 -0
- package/dist/src/nugget-lookup.js +140 -0
- package/dist/src/onboarding.js +357 -0
- package/dist/src/outbound.js +17 -0
- package/dist/src/reply-dispatcher.js +46 -0
- package/dist/src/runtime.js +5 -0
- package/dist/src/setup-core.js +46 -0
- package/dist/src/setup-surface.js +2 -0
- package/dist/src/socket/dolphin.socket.js +18 -0
- package/dist/src/socket/jaguar.socket.js +22 -0
- package/dist/src/socket/socket.js +153 -0
- package/dist/src/store.js +13 -0
- package/dist/src/token.js +84 -0
- package/dist/src/types.js +15 -0
- package/dist/src/utils/http.util.js +43 -0
- package/dist/src/utils/jwt.util.js +15 -0
- package/package.json +10 -3
- package/src/api/auth.api.ts +27 -7
- package/src/channel.ts +127 -238
- package/src/member-resolve.ts +1 -1
- package/src/onboarding.ts +117 -163
- package/src/setup-core.ts +10 -1
- package/src/socket/socket.ts +24 -11
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { OMADEUS_INBOUND_ENTITY_KIND_SET } from "./types.js";
|
|
2
|
+
//#region src/inbound-policy.ts
|
|
3
|
+
/** Default inbound policy when `channels.omadeus.inbound` is absent. */
|
|
4
|
+
const DEFAULT_INBOUND_POLICY = {
|
|
5
|
+
version: 1,
|
|
6
|
+
direct: {
|
|
7
|
+
enabled: true,
|
|
8
|
+
requireMention: "never"
|
|
9
|
+
},
|
|
10
|
+
channels: {
|
|
11
|
+
enabled: false,
|
|
12
|
+
requireMention: "outsideAllowlist"
|
|
13
|
+
},
|
|
14
|
+
entities: {
|
|
15
|
+
enabled: false,
|
|
16
|
+
requireMention: "always"
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
function mergePolicy(cfg) {
|
|
20
|
+
const inbound = cfg?.inbound;
|
|
21
|
+
return {
|
|
22
|
+
version: typeof inbound?.version === "number" && inbound.version >= 1 ? inbound.version : 1,
|
|
23
|
+
direct: {
|
|
24
|
+
...DEFAULT_INBOUND_POLICY.direct,
|
|
25
|
+
...inbound?.direct,
|
|
26
|
+
requireMention: inbound?.direct?.requireMention ?? DEFAULT_INBOUND_POLICY.direct.requireMention
|
|
27
|
+
},
|
|
28
|
+
channels: {
|
|
29
|
+
...DEFAULT_INBOUND_POLICY.channels,
|
|
30
|
+
...inbound?.channels,
|
|
31
|
+
requireMention: inbound?.channels?.requireMention ?? DEFAULT_INBOUND_POLICY.channels.requireMention
|
|
32
|
+
},
|
|
33
|
+
entities: {
|
|
34
|
+
...DEFAULT_INBOUND_POLICY.entities,
|
|
35
|
+
...inbound?.entities,
|
|
36
|
+
requireMention: inbound?.entities?.requireMention ?? DEFAULT_INBOUND_POLICY.entities.requireMention
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function surfaceForKind(kind) {
|
|
41
|
+
if (kind === "direct") return "direct";
|
|
42
|
+
if (kind === "channel") return "channel";
|
|
43
|
+
return "entity";
|
|
44
|
+
}
|
|
45
|
+
function senderAllowed(allowed, fromReferenceId) {
|
|
46
|
+
if (!allowed || allowed.length === 0) return true;
|
|
47
|
+
return allowed.includes(fromReferenceId);
|
|
48
|
+
}
|
|
49
|
+
function channelGeoAllowed(params) {
|
|
50
|
+
const { roomId, channelViewId, allowedRoomIds = [], allowedChannelViewIds = [] } = params;
|
|
51
|
+
const hasRooms = allowedRoomIds.length > 0;
|
|
52
|
+
const hasViews = allowedChannelViewIds.length > 0;
|
|
53
|
+
let geoInAllowlist = true;
|
|
54
|
+
if (hasRooms && hasViews) geoInAllowlist = allowedRoomIds.includes(roomId) || channelViewId !== void 0 && allowedChannelViewIds.includes(channelViewId);
|
|
55
|
+
else if (hasRooms) geoInAllowlist = allowedRoomIds.includes(roomId);
|
|
56
|
+
else if (hasViews) geoInAllowlist = channelViewId !== void 0 && allowedChannelViewIds.includes(channelViewId);
|
|
57
|
+
return {
|
|
58
|
+
geoInAllowlist,
|
|
59
|
+
details: {
|
|
60
|
+
roomId,
|
|
61
|
+
channelViewId,
|
|
62
|
+
allowedRoomIds,
|
|
63
|
+
allowedChannelViewIds,
|
|
64
|
+
hasRooms,
|
|
65
|
+
hasViews,
|
|
66
|
+
geoInAllowlist
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function entityKindAllowed(kind, allowedKinds) {
|
|
71
|
+
if (!allowedKinds || allowedKinds.length === 0) return OMADEUS_INBOUND_ENTITY_KIND_SET.has(String(kind));
|
|
72
|
+
return allowedKinds.includes(String(kind));
|
|
73
|
+
}
|
|
74
|
+
function entityRoomOk(roomId, allowedRoomIds) {
|
|
75
|
+
if (!allowedRoomIds || allowedRoomIds.length === 0) return true;
|
|
76
|
+
return allowedRoomIds.includes(roomId);
|
|
77
|
+
}
|
|
78
|
+
function mentionRequired(params) {
|
|
79
|
+
const requireMention = params.requireMention ?? "never";
|
|
80
|
+
const { inAllowlist, isMention } = params;
|
|
81
|
+
if (requireMention === "never") return false;
|
|
82
|
+
if (requireMention === "always") return !isMention;
|
|
83
|
+
if (inAllowlist) return false;
|
|
84
|
+
return !isMention;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Evaluate whether a normalized Jaguar inbound should be dispatched to OpenClaw.
|
|
88
|
+
* Callers must drop self-authored messages separately if they prefer logging there.
|
|
89
|
+
*/
|
|
90
|
+
function evaluateOmadeusInboundPolicy(params) {
|
|
91
|
+
const { inbound, omadeusCfg, selfReferenceId } = params;
|
|
92
|
+
if (inbound.fromReferenceId === selfReferenceId) return {
|
|
93
|
+
allow: false,
|
|
94
|
+
reason: "self_message",
|
|
95
|
+
details: {
|
|
96
|
+
fromReferenceId: inbound.fromReferenceId,
|
|
97
|
+
selfReferenceId
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
const policy = mergePolicy(omadeusCfg);
|
|
101
|
+
const surface = surfaceForKind(inbound.subscribableKind);
|
|
102
|
+
if (surface === "direct") {
|
|
103
|
+
if (!policy.direct.enabled) return {
|
|
104
|
+
allow: false,
|
|
105
|
+
reason: "direct_disabled",
|
|
106
|
+
details: { surface }
|
|
107
|
+
};
|
|
108
|
+
if (!senderAllowed(policy.direct.allowedSenderReferenceIds, inbound.fromReferenceId)) return {
|
|
109
|
+
allow: false,
|
|
110
|
+
reason: "direct_sender_not_allowed",
|
|
111
|
+
details: { fromReferenceId: inbound.fromReferenceId }
|
|
112
|
+
};
|
|
113
|
+
const req = policy.direct.requireMention ?? "never";
|
|
114
|
+
if (mentionRequired({
|
|
115
|
+
requireMention: req,
|
|
116
|
+
inAllowlist: true,
|
|
117
|
+
isMention: inbound.isMention
|
|
118
|
+
})) return {
|
|
119
|
+
allow: false,
|
|
120
|
+
reason: "direct_mention_required",
|
|
121
|
+
details: { requireMention: req }
|
|
122
|
+
};
|
|
123
|
+
return { allow: true };
|
|
124
|
+
}
|
|
125
|
+
if (surface === "channel") {
|
|
126
|
+
if (!policy.channels.enabled) return {
|
|
127
|
+
allow: false,
|
|
128
|
+
reason: "channels_disabled",
|
|
129
|
+
details: { surface }
|
|
130
|
+
};
|
|
131
|
+
if (!senderAllowed(policy.channels.allowedSenderReferenceIds, inbound.fromReferenceId)) return {
|
|
132
|
+
allow: false,
|
|
133
|
+
reason: "channel_sender_not_allowed",
|
|
134
|
+
details: { fromReferenceId: inbound.fromReferenceId }
|
|
135
|
+
};
|
|
136
|
+
const rv = channelGeoAllowed({
|
|
137
|
+
roomId: inbound.roomId,
|
|
138
|
+
channelViewId: inbound.channelViewId,
|
|
139
|
+
allowedRoomIds: policy.channels.allowedRoomIds,
|
|
140
|
+
allowedChannelViewIds: policy.channels.allowedChannelViewIds
|
|
141
|
+
});
|
|
142
|
+
const senderInList = !policy.channels.allowedSenderReferenceIds || policy.channels.allowedSenderReferenceIds.length === 0 || policy.channels.allowedSenderReferenceIds.includes(inbound.fromReferenceId);
|
|
143
|
+
const inAllowlist = rv.geoInAllowlist && senderInList;
|
|
144
|
+
const channelMention = policy.channels.requireMention ?? DEFAULT_INBOUND_POLICY.channels.requireMention;
|
|
145
|
+
if (mentionRequired({
|
|
146
|
+
requireMention: channelMention,
|
|
147
|
+
inAllowlist,
|
|
148
|
+
isMention: inbound.isMention
|
|
149
|
+
})) return {
|
|
150
|
+
allow: false,
|
|
151
|
+
reason: "channel_mention_required",
|
|
152
|
+
details: {
|
|
153
|
+
requireMention: channelMention,
|
|
154
|
+
inAllowlist,
|
|
155
|
+
isMention: inbound.isMention
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
return { allow: true };
|
|
159
|
+
}
|
|
160
|
+
if (!policy.entities.enabled) return {
|
|
161
|
+
allow: false,
|
|
162
|
+
reason: "entities_disabled",
|
|
163
|
+
details: { kind: inbound.subscribableKind }
|
|
164
|
+
};
|
|
165
|
+
if (!entityKindAllowed(inbound.subscribableKind, policy.entities.allowedKinds)) return {
|
|
166
|
+
allow: false,
|
|
167
|
+
reason: "entity_kind_not_allowed",
|
|
168
|
+
details: {
|
|
169
|
+
kind: inbound.subscribableKind,
|
|
170
|
+
allowedKinds: policy.entities.allowedKinds
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
if (!senderAllowed(policy.entities.allowedSenderReferenceIds, inbound.fromReferenceId)) return {
|
|
174
|
+
allow: false,
|
|
175
|
+
reason: "entity_sender_not_allowed",
|
|
176
|
+
details: { fromReferenceId: inbound.fromReferenceId }
|
|
177
|
+
};
|
|
178
|
+
if (!entityRoomOk(inbound.roomId, policy.entities.allowedRoomIds)) return {
|
|
179
|
+
allow: false,
|
|
180
|
+
reason: "entity_room_not_allowed",
|
|
181
|
+
details: {
|
|
182
|
+
roomId: inbound.roomId,
|
|
183
|
+
allowedRoomIds: policy.entities.allowedRoomIds
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
const roomList = policy.entities.allowedRoomIds ?? [];
|
|
187
|
+
const inAllowlist = roomList.length === 0 || roomList.includes(inbound.roomId);
|
|
188
|
+
const entityMention = policy.entities.requireMention ?? DEFAULT_INBOUND_POLICY.entities.requireMention;
|
|
189
|
+
if (mentionRequired({
|
|
190
|
+
requireMention: entityMention,
|
|
191
|
+
inAllowlist,
|
|
192
|
+
isMention: inbound.isMention
|
|
193
|
+
})) return {
|
|
194
|
+
allow: false,
|
|
195
|
+
reason: "entity_mention_required",
|
|
196
|
+
details: {
|
|
197
|
+
requireMention: entityMention,
|
|
198
|
+
inAllowlist,
|
|
199
|
+
isMention: inbound.isMention
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
return { allow: true };
|
|
203
|
+
}
|
|
204
|
+
//#endregion
|
|
205
|
+
export { evaluateOmadeusInboundPolicy };
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
//#region src/inbound.ts
|
|
2
|
+
const USER_REF_PATTERN = /\{user_reference_id:(\d+)\}/g;
|
|
3
|
+
const BOLD_MENTION_PREFIX_PATTERN = /^\*\*@[^*]+\*\*/;
|
|
4
|
+
/**
|
|
5
|
+
* Parse the `details` JSON string to extract the rawMessage with mention
|
|
6
|
+
* template tokens like `{user_reference_id:87}`.
|
|
7
|
+
*/
|
|
8
|
+
function parseDetails(raw) {
|
|
9
|
+
if (!raw) return null;
|
|
10
|
+
try {
|
|
11
|
+
return JSON.parse(raw);
|
|
12
|
+
} catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Check whether the bot user (by referenceId) is @-mentioned in the message.
|
|
18
|
+
* Omadeus encodes mentions as `{user_reference_id:N}` in details.rawMessage.
|
|
19
|
+
*/
|
|
20
|
+
function isBotMentioned(details, selfReferenceId) {
|
|
21
|
+
const raw = details?.rawMessage;
|
|
22
|
+
if (!raw) return false;
|
|
23
|
+
for (const match of raw.matchAll(USER_REF_PATTERN)) if (Number(match[1]) === selfReferenceId) return true;
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Fallback mention detection when details.rawMessage is absent.
|
|
28
|
+
* Omadeus often prefixes mentioned messages with `**@Display Name** ...`.
|
|
29
|
+
*/
|
|
30
|
+
function hasMentionPrefixInBody(body) {
|
|
31
|
+
return BOLD_MENTION_PREFIX_PATTERN.test(body.trim());
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Strip the formatted @mention from the body so the agent sees clean text.
|
|
35
|
+
* The body contains `**@Display Name** actual text`; this strips the bold
|
|
36
|
+
* mention prefix when it appears at the start.
|
|
37
|
+
*/
|
|
38
|
+
function stripLeadingMention(body) {
|
|
39
|
+
return body.replace(/^\*\*@[^*]+\*\*\s*/, "").trim();
|
|
40
|
+
}
|
|
41
|
+
function readChannelViewId(metadata) {
|
|
42
|
+
if (metadata === null || metadata === void 0 || typeof metadata !== "object") return;
|
|
43
|
+
const m = metadata;
|
|
44
|
+
for (const key of [
|
|
45
|
+
"channelViewId",
|
|
46
|
+
"channel_view_id",
|
|
47
|
+
"viewId",
|
|
48
|
+
"subscribableViewId"
|
|
49
|
+
]) {
|
|
50
|
+
const v = m[key];
|
|
51
|
+
if (typeof v === "number" && Number.isFinite(v)) return v;
|
|
52
|
+
if (typeof v === "string" && /^\d+$/.test(v.trim())) return Number(v.trim());
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Determine whether a raw Jaguar socket payload is an OmadeusMessage.
|
|
57
|
+
*/
|
|
58
|
+
function isOmadeusMessage(data) {
|
|
59
|
+
if (typeof data !== "object" || data === null) return false;
|
|
60
|
+
const obj = data;
|
|
61
|
+
return obj.type === "message" && typeof obj.roomId === "number" && typeof obj.body === "string";
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Parse a Jaguar socket message into an OpenClaw inbound message.
|
|
65
|
+
*
|
|
66
|
+
* Returns null when:
|
|
67
|
+
* - The event is not a chat message
|
|
68
|
+
* - The body is empty or the message was removed
|
|
69
|
+
*/
|
|
70
|
+
function parseJaguarMessage(msg, opts, log) {
|
|
71
|
+
if (msg.type !== "message") {
|
|
72
|
+
log?.debug?.(`[jaguar-inbound] ignoring type: ${msg.type}`);
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
if (msg.removedAt) return null;
|
|
76
|
+
const body = (msg.body ?? "").trim();
|
|
77
|
+
if (!body) return null;
|
|
78
|
+
const mentioned = isBotMentioned(parseDetails(msg.details), opts.selfReferenceId) || hasMentionPrefixInBody(body);
|
|
79
|
+
const content = mentioned ? stripLeadingMention(body) : body;
|
|
80
|
+
if (!content) return null;
|
|
81
|
+
const channelViewId = readChannelViewId(msg.metadata);
|
|
82
|
+
return {
|
|
83
|
+
messageId: msg.id,
|
|
84
|
+
from: String(msg.senderReferenceId),
|
|
85
|
+
fromReferenceId: msg.senderReferenceId,
|
|
86
|
+
content,
|
|
87
|
+
roomId: msg.roomId,
|
|
88
|
+
roomName: msg.roomName,
|
|
89
|
+
subscribableType: msg.subscribableType,
|
|
90
|
+
subscribableKind: msg.subscribableKind,
|
|
91
|
+
...channelViewId !== void 0 ? { channelViewId } : {},
|
|
92
|
+
isMention: mentioned,
|
|
93
|
+
timestamp: msg.createdAtTimestamp ? Math.floor(msg.createdAtTimestamp * 1e3) : Date.now()
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
//#endregion
|
|
97
|
+
export { isOmadeusMessage, parseJaguarMessage };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { listOrganizationMembers } from "./api/auth.api.js";
|
|
2
|
+
//#region src/member-resolve.ts
|
|
3
|
+
function formatMemberLabel(m) {
|
|
4
|
+
const fullName = `${m.firstName ?? ""} ${m.lastName ?? ""}`.trim();
|
|
5
|
+
if (fullName) return fullName;
|
|
6
|
+
if (m.title?.trim()) return m.title.trim();
|
|
7
|
+
if (m.email?.trim()) return m.email.trim();
|
|
8
|
+
return `Member ${m.referenceId}`;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Resolves Omadeus `referenceId` → human-readable label for the current organization (session JWT).
|
|
12
|
+
* Used so agents do not echo raw ids like 210 to users.
|
|
13
|
+
*/
|
|
14
|
+
async function buildReferenceIdNameMap(apiOpts) {
|
|
15
|
+
const { organizationId } = apiOpts.tokenManager.getPayload();
|
|
16
|
+
const sessionToken = apiOpts.tokenManager.getToken();
|
|
17
|
+
const members = await listOrganizationMembers({
|
|
18
|
+
maestroUrl: apiOpts.maestroUrl,
|
|
19
|
+
sessionToken,
|
|
20
|
+
organizationId
|
|
21
|
+
});
|
|
22
|
+
const map = /* @__PURE__ */ new Map();
|
|
23
|
+
for (const m of members) map.set(m.referenceId, formatMemberLabel(m));
|
|
24
|
+
return map;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Adds a `people` object mapping each `*ReferenceId` field in the nugget row to a display name.
|
|
28
|
+
* Keys match the source field names (e.g. `memberReferenceId: "Pat Example"`).
|
|
29
|
+
*/
|
|
30
|
+
async function mergePeopleIntoNuggetAgentPayload(apiOpts, fullRecord, basePayload) {
|
|
31
|
+
let nameByRef;
|
|
32
|
+
try {
|
|
33
|
+
nameByRef = await buildReferenceIdNameMap(apiOpts);
|
|
34
|
+
} catch {
|
|
35
|
+
return { ...basePayload };
|
|
36
|
+
}
|
|
37
|
+
const people = {};
|
|
38
|
+
for (const [key, v] of Object.entries(fullRecord)) {
|
|
39
|
+
if (!/referenceid$/i.test(key)) continue;
|
|
40
|
+
const id = typeof v === "number" && Number.isFinite(v) ? v : typeof v === "string" && /^\d+$/.test(v.trim()) ? Number(v.trim()) : NaN;
|
|
41
|
+
if (!Number.isFinite(id)) continue;
|
|
42
|
+
const name = nameByRef.get(id);
|
|
43
|
+
if (name) people[key] = name;
|
|
44
|
+
}
|
|
45
|
+
const out = { ...basePayload };
|
|
46
|
+
if (Object.keys(people).length > 0) {
|
|
47
|
+
out.people = people;
|
|
48
|
+
for (const key of Object.keys(people)) if (key in out) delete out[key];
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
//#endregion
|
|
53
|
+
export { formatMemberLabel, mergePeopleIntoNuggetAgentPayload };
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { logInboundDrop, resolveControlCommandGate } from "../runtime-api.js";
|
|
2
|
+
import { createNugget, findNuggetByTaskChannelRoom, resolveTaskChannelRoomId, searchNuggetByNumber } from "./api/nugget.api.js";
|
|
3
|
+
import { getOmadeusChannelConfig } from "./config.js";
|
|
4
|
+
import { appendNuggetContextForTaskOrNuggetRoom, appendNuggetLookupContextForAgent, parseChannelTaskCreateIntent, parseNuggetLookupIntent, parseRecurringScheduleIntent } from "./nugget-lookup.js";
|
|
5
|
+
import { getOmadeusRuntime } from "./runtime.js";
|
|
6
|
+
import { createOmadeusReplyDispatcher } from "./reply-dispatcher.js";
|
|
7
|
+
import { OMADEUS_INBOUND_ENTITY_KIND_SET } from "./types.js";
|
|
8
|
+
import { evaluateOmadeusInboundPolicy } from "./inbound-policy.js";
|
|
9
|
+
//#region src/message-handler.ts
|
|
10
|
+
const SK = "`subscribableKind`";
|
|
11
|
+
/** Injected into BodyForAgent so the model uses Jaguar room kind, not invented OpenClaw `task/...` keys. */
|
|
12
|
+
function buildOmadeusEntityRoomContextLine(kind) {
|
|
13
|
+
if (kind === "task") return `This chat is an **Omadeus Task** room (${SK} \`task\`). If the user asks about the Task, its status, or says \"the task\", they mean **this** Omadeus Task / this thread — not an OpenClaw \"task\" or a \"session id\" for it.`;
|
|
14
|
+
if (kind === "nugget") return `This chat is an **Omadeus Nugget** room (${SK} \`nugget\`). If the user asks about the Nugget, its status, or colloquially says \"the task\", they mean **this** Omadeus Nugget / this thread — not an OpenClaw \"task\" or a \"session id\" for it. (Task and Nugget are different; infer from this room's ${SK}.)`;
|
|
15
|
+
const label = {
|
|
16
|
+
project: "Project",
|
|
17
|
+
release: "Release",
|
|
18
|
+
sprint: "Sprint",
|
|
19
|
+
summary: "Summary",
|
|
20
|
+
client: "Client",
|
|
21
|
+
folder: "Folder"
|
|
22
|
+
}[kind] ?? kind;
|
|
23
|
+
return `This chat is an **Omadeus ${label}** room (${SK} \`${kind}\`). User questions refer to that Omadeus ${label} in this thread — not an OpenClaw \"session id\" for it.`;
|
|
24
|
+
}
|
|
25
|
+
function shouldSkipTaskRoomNuggetFetch(rawBody) {
|
|
26
|
+
const t = rawBody.trim();
|
|
27
|
+
if (!t) return true;
|
|
28
|
+
if (/^(ok|k|thanks?|thank you|ty|sounds good|got it|yes|yep|no|nope)\s*!*\.?$/i.test(t)) return true;
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
function createOmadeusMessageHandler(deps) {
|
|
32
|
+
const { cfg, runtime, log, outboundDeps, selfReferenceId } = deps;
|
|
33
|
+
const core = getOmadeusRuntime();
|
|
34
|
+
const omadeusCfg = getOmadeusChannelConfig(cfg);
|
|
35
|
+
const inboundDebounceMs = core.channel.debounce.resolveInboundDebounceMs({
|
|
36
|
+
cfg,
|
|
37
|
+
channel: "omadeus"
|
|
38
|
+
});
|
|
39
|
+
const handleMessageNow = async (inbound) => {
|
|
40
|
+
const isDirectMessage = inbound.subscribableKind === "direct";
|
|
41
|
+
const senderId = String(inbound.fromReferenceId);
|
|
42
|
+
const senderName = inbound.from;
|
|
43
|
+
const roomId = String(inbound.roomId);
|
|
44
|
+
const rawBody = inbound.content;
|
|
45
|
+
if (!rawBody.trim()) {
|
|
46
|
+
log.debug?.("skipping empty message");
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const policyDecision = evaluateOmadeusInboundPolicy({
|
|
50
|
+
inbound,
|
|
51
|
+
omadeusCfg,
|
|
52
|
+
selfReferenceId
|
|
53
|
+
});
|
|
54
|
+
if (!policyDecision.allow) {
|
|
55
|
+
log.info("omadeus: dropped message by inbound policy", {
|
|
56
|
+
reason: policyDecision.reason,
|
|
57
|
+
...policyDecision.details ?? {},
|
|
58
|
+
roomId: inbound.roomId,
|
|
59
|
+
kind: inbound.subscribableKind,
|
|
60
|
+
fromReferenceId: inbound.fromReferenceId,
|
|
61
|
+
isMention: inbound.isMention
|
|
62
|
+
});
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const commandGate = resolveControlCommandGate({
|
|
66
|
+
useAccessGroups: cfg.commands?.useAccessGroups !== false,
|
|
67
|
+
authorizers: [],
|
|
68
|
+
allowTextCommands: true,
|
|
69
|
+
hasControlCommand: core.channel.text.hasControlCommand(rawBody, cfg)
|
|
70
|
+
});
|
|
71
|
+
if (commandGate.shouldBlock) {
|
|
72
|
+
logInboundDrop({
|
|
73
|
+
log: (msg) => log.debug?.(msg),
|
|
74
|
+
channel: "omadeus",
|
|
75
|
+
reason: "control command (unauthorized)",
|
|
76
|
+
target: senderId
|
|
77
|
+
});
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
let bodyForAgent = rawBody;
|
|
81
|
+
const createIntent = parseChannelTaskCreateIntent(rawBody);
|
|
82
|
+
if (createIntent) try {
|
|
83
|
+
const memberReferenceId = inbound.fromReferenceId;
|
|
84
|
+
const created = await createNugget(outboundDeps.apiOpts, {
|
|
85
|
+
title: createIntent.title,
|
|
86
|
+
description: createIntent.description,
|
|
87
|
+
kind: createIntent.kind,
|
|
88
|
+
priority: createIntent.priority,
|
|
89
|
+
stage: "Triage",
|
|
90
|
+
memberReferenceId,
|
|
91
|
+
clientId: 1,
|
|
92
|
+
folderId: 1
|
|
93
|
+
});
|
|
94
|
+
const createdLabel = typeof created["number"] === "number" ? `N${created["number"]}` : String(created["id"] ?? "created");
|
|
95
|
+
const recurring = parseRecurringScheduleIntent(rawBody);
|
|
96
|
+
if (recurring && typeof created["number"] === "number") {
|
|
97
|
+
const cronExpr = recurring.everyMinutes === 60 ? "0 * * * *" : `*/${recurring.everyMinutes} * * * *`;
|
|
98
|
+
const taskRoomId = resolveTaskChannelRoomId(created);
|
|
99
|
+
const taskTarget = taskRoomId ? `room:${taskRoomId}` : `N${created["number"]}`;
|
|
100
|
+
bodyForAgent = `${rawBody}\n\n[Omadeus create] Created ${createIntent.kind} ${createdLabel}.\n[Scheduling required] The user asked for recurring execution.\nYou MUST use the cron tool now (no simulation) to add a job with:\n- schedule.kind: "cron"\n- schedule.expr: "${cronExpr}"\n- payload.kind: "agentTurn"\n- payload.message: "${createIntent.description}"\n- payload.deliver: true\n- payload.channel: "omadeus"\n- payload.to: "${taskTarget}"\n- sessionTarget: "isolated"\n- delivery.mode: "announce"\n- delivery.channel: "omadeus"\n- delivery.to: "${taskTarget}"\nDo NOT deliver to the current selected channel; delivery must go only to the created task private channel target above.\nThen confirm cron job creation to the user.`;
|
|
101
|
+
} else bodyForAgent = `${rawBody}\n\n[Omadeus create] Created ${createIntent.kind} ${createdLabel}.`;
|
|
102
|
+
} catch (err) {
|
|
103
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
104
|
+
runtime.error?.(`omadeus channel-triggered task create failed: ${errorMessage}`);
|
|
105
|
+
}
|
|
106
|
+
const nuggetIntent = parseNuggetLookupIntent(rawBody);
|
|
107
|
+
if (nuggetIntent) try {
|
|
108
|
+
const nugget = await searchNuggetByNumber(outboundDeps.apiOpts, { nuggetNumber: nuggetIntent.nuggetNumber });
|
|
109
|
+
bodyForAgent = await appendNuggetLookupContextForAgent(rawBody, nuggetIntent.nuggetNumber, nugget, outboundDeps.apiOpts);
|
|
110
|
+
} catch (err) {
|
|
111
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
112
|
+
runtime.error?.(`omadeus nugget lookup failed: ${errorMessage}`);
|
|
113
|
+
bodyForAgent = await appendNuggetLookupContextForAgent(rawBody, nuggetIntent.nuggetNumber, null, outboundDeps.apiOpts, errorMessage);
|
|
114
|
+
}
|
|
115
|
+
if (!isDirectMessage && (inbound.subscribableKind === "task" || inbound.subscribableKind === "nugget") && !nuggetIntent && !createIntent && !shouldSkipTaskRoomNuggetFetch(rawBody)) try {
|
|
116
|
+
const nugget = await findNuggetByTaskChannelRoom(outboundDeps.apiOpts, {
|
|
117
|
+
roomId: inbound.roomId,
|
|
118
|
+
roomName: inbound.roomName
|
|
119
|
+
});
|
|
120
|
+
bodyForAgent = await appendNuggetContextForTaskOrNuggetRoom(bodyForAgent, inbound.roomId, inbound.roomName, nugget, outboundDeps.apiOpts);
|
|
121
|
+
} catch (err) {
|
|
122
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
123
|
+
runtime.error?.(`omadeus task room nugget lookup failed: ${errorMessage}`);
|
|
124
|
+
bodyForAgent = await appendNuggetContextForTaskOrNuggetRoom(bodyForAgent, inbound.roomId, inbound.roomName, null, outboundDeps.apiOpts, errorMessage);
|
|
125
|
+
}
|
|
126
|
+
const omadeusFrom = isDirectMessage ? `omadeus:${senderId}` : `omadeus:group:${roomId}`;
|
|
127
|
+
const omadeusTo = isDirectMessage ? `room:${roomId}` : `room:${roomId}`;
|
|
128
|
+
const route = core.channel.routing.resolveAgentRoute({
|
|
129
|
+
cfg,
|
|
130
|
+
channel: "omadeus",
|
|
131
|
+
peer: {
|
|
132
|
+
kind: isDirectMessage ? "direct" : "group",
|
|
133
|
+
id: isDirectMessage ? senderId : roomId
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
if (!isDirectMessage && OMADEUS_INBOUND_ENTITY_KIND_SET.has(String(inbound.subscribableKind))) {
|
|
137
|
+
const entityLine = buildOmadeusEntityRoomContextLine(String(inbound.subscribableKind));
|
|
138
|
+
bodyForAgent = `${bodyForAgent}\n\n[OpenClaw] ${entityLine} \`session_status\` is only for **OpenClaw** gateway session state (model/usage, etc.); for that, use this key or \`current\`: ${route.sessionKey} — never \`task/\` + a title as a session key.`;
|
|
139
|
+
}
|
|
140
|
+
const preview = rawBody.replace(/\s+/g, " ").slice(0, 160);
|
|
141
|
+
const inboundLabel = isDirectMessage ? `Omadeus DM from ${senderName}` : `Omadeus message in ${inbound.subscribableKind}/${inbound.roomName ?? roomId} from ${senderName}`;
|
|
142
|
+
core.system.enqueueSystemEvent(`${inboundLabel}: ${preview}`, {
|
|
143
|
+
sessionKey: route.sessionKey,
|
|
144
|
+
contextKey: `omadeus:message:${roomId}:${inbound.timestamp}`
|
|
145
|
+
});
|
|
146
|
+
const envelopeFrom = isDirectMessage ? senderName : inbound.roomName ?? roomId;
|
|
147
|
+
const storePath = core.channel.session.resolveStorePath(cfg.session?.store, { agentId: route.agentId });
|
|
148
|
+
const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(cfg);
|
|
149
|
+
const timestamp = inbound.timestamp ? new Date(inbound.timestamp) : void 0;
|
|
150
|
+
const previousTimestamp = core.channel.session.readSessionUpdatedAt({
|
|
151
|
+
storePath,
|
|
152
|
+
sessionKey: route.sessionKey
|
|
153
|
+
});
|
|
154
|
+
const body = core.channel.reply.formatAgentEnvelope({
|
|
155
|
+
channel: "Omadeus",
|
|
156
|
+
from: envelopeFrom,
|
|
157
|
+
timestamp,
|
|
158
|
+
previousTimestamp,
|
|
159
|
+
envelope: envelopeOptions,
|
|
160
|
+
body: rawBody
|
|
161
|
+
});
|
|
162
|
+
const ctxPayload = core.channel.reply.finalizeInboundContext({
|
|
163
|
+
Body: body,
|
|
164
|
+
BodyForAgent: bodyForAgent,
|
|
165
|
+
RawBody: rawBody,
|
|
166
|
+
CommandBody: rawBody.trim(),
|
|
167
|
+
BodyForCommands: rawBody.trim(),
|
|
168
|
+
/** Lets the message tool default `react` / `edit` to this Jaguar message id. */
|
|
169
|
+
MessageSid: String(inbound.messageId),
|
|
170
|
+
From: omadeusFrom,
|
|
171
|
+
To: omadeusTo,
|
|
172
|
+
SessionKey: route.sessionKey,
|
|
173
|
+
AccountId: route.accountId,
|
|
174
|
+
ChatType: isDirectMessage ? "direct" : "group",
|
|
175
|
+
ConversationLabel: envelopeFrom,
|
|
176
|
+
GroupSubject: !isDirectMessage ? inbound.roomName ?? inbound.subscribableKind : void 0,
|
|
177
|
+
SenderName: senderName,
|
|
178
|
+
SenderId: senderId,
|
|
179
|
+
Provider: "omadeus",
|
|
180
|
+
Surface: "omadeus",
|
|
181
|
+
Timestamp: inbound.timestamp ?? Date.now(),
|
|
182
|
+
WasMentioned: isDirectMessage || inbound.isMention,
|
|
183
|
+
CommandAuthorized: commandGate.commandAuthorized,
|
|
184
|
+
OriginatingChannel: "omadeus",
|
|
185
|
+
OriginatingTo: omadeusTo
|
|
186
|
+
});
|
|
187
|
+
await core.channel.session.recordInboundSession({
|
|
188
|
+
storePath,
|
|
189
|
+
sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
|
|
190
|
+
ctx: ctxPayload,
|
|
191
|
+
onRecordError: (err) => {
|
|
192
|
+
log.debug?.(`omadeus: failed updating session meta: ${String(err)}`);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
const { dispatcher, replyOptions, markDispatchIdle } = createOmadeusReplyDispatcher({
|
|
196
|
+
cfg,
|
|
197
|
+
agentId: route.agentId,
|
|
198
|
+
accountId: route.accountId,
|
|
199
|
+
runtime,
|
|
200
|
+
log,
|
|
201
|
+
outboundDeps,
|
|
202
|
+
roomId
|
|
203
|
+
});
|
|
204
|
+
log.info("dispatching to agent", { sessionKey: route.sessionKey });
|
|
205
|
+
try {
|
|
206
|
+
const { queuedFinal, counts } = await core.channel.reply.withReplyDispatcher({
|
|
207
|
+
dispatcher,
|
|
208
|
+
onSettled: () => {
|
|
209
|
+
markDispatchIdle();
|
|
210
|
+
},
|
|
211
|
+
run: () => core.channel.reply.dispatchReplyFromConfig({
|
|
212
|
+
ctx: ctxPayload,
|
|
213
|
+
cfg,
|
|
214
|
+
dispatcher,
|
|
215
|
+
replyOptions
|
|
216
|
+
})
|
|
217
|
+
});
|
|
218
|
+
log.info("dispatch complete", {
|
|
219
|
+
queuedFinal,
|
|
220
|
+
counts
|
|
221
|
+
});
|
|
222
|
+
const finalCount = counts.final;
|
|
223
|
+
if (queuedFinal) log.debug?.(`omadeus: delivered ${finalCount} repl${finalCount === 1 ? "y" : "ies"} to room ${roomId}`);
|
|
224
|
+
} catch (err) {
|
|
225
|
+
log.error("dispatch failed", { error: String(err) });
|
|
226
|
+
runtime.error?.(`omadeus dispatch failed: ${String(err)}`);
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
const inboundDebouncer = core.channel.debounce.createInboundDebouncer({
|
|
230
|
+
debounceMs: inboundDebounceMs,
|
|
231
|
+
buildKey: (entry) => {
|
|
232
|
+
return `omadeus:${entry.roomId}:${entry.fromReferenceId}`;
|
|
233
|
+
},
|
|
234
|
+
shouldDebounce: (entry) => {
|
|
235
|
+
if (!entry.content.trim()) return false;
|
|
236
|
+
return !core.channel.text.hasControlCommand(entry.content, cfg);
|
|
237
|
+
},
|
|
238
|
+
onFlush: async (entries) => {
|
|
239
|
+
const last = entries.at(-1);
|
|
240
|
+
if (!last) return;
|
|
241
|
+
if (entries.length === 1) {
|
|
242
|
+
await handleMessageNow(last);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
const combinedContent = entries.map((e) => e.content).filter(Boolean).join("\n");
|
|
246
|
+
if (!combinedContent.trim()) return;
|
|
247
|
+
await handleMessageNow({
|
|
248
|
+
...last,
|
|
249
|
+
content: combinedContent,
|
|
250
|
+
isMention: entries.some((e) => e.isMention)
|
|
251
|
+
});
|
|
252
|
+
},
|
|
253
|
+
onError: (err) => {
|
|
254
|
+
runtime.error?.(`omadeus debounce flush failed: ${String(err)}`);
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
return async function handleOmadeusMessage(inbound) {
|
|
258
|
+
await inboundDebouncer.enqueue(inbound);
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
//#endregion
|
|
262
|
+
export { createOmadeusMessageHandler };
|