@brantrusnak/openclaw-omadeus 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/openclaw.plugin.json +144 -28
- package/package.json +3 -5
- package/src/api/auth.api.ts +0 -29
- package/src/api/channel.api.ts +29 -0
- package/src/api/nugget.api.ts +81 -10
- package/src/channel.ts +9 -9
- package/src/inbound-policy.ts +250 -0
- package/src/inbound.ts +20 -0
- package/src/member-resolve.ts +84 -0
- package/src/message-handler.ts +99 -53
- package/src/nugget-lookup.ts +67 -4
- package/src/onboarding.ts +242 -120
- package/src/types.ts +77 -7
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type OmadeusChannelConfig,
|
|
3
|
+
type OmadeusInboundMessage,
|
|
4
|
+
type OmadeusInboundPolicy,
|
|
5
|
+
OMADEUS_INBOUND_ENTITY_KIND_SET,
|
|
6
|
+
type OmadeusSubscribableKind,
|
|
7
|
+
} from "./types.js";
|
|
8
|
+
|
|
9
|
+
/** Default inbound policy when `channels.omadeus.inbound` is absent. */
|
|
10
|
+
export const DEFAULT_INBOUND_POLICY: Required<
|
|
11
|
+
Pick<OmadeusInboundPolicy, "direct" | "channels" | "entities">
|
|
12
|
+
> & { version: number } = {
|
|
13
|
+
version: 1,
|
|
14
|
+
direct: { enabled: true, requireMention: "never" },
|
|
15
|
+
channels: { enabled: false, requireMention: "outsideAllowlist" },
|
|
16
|
+
entities: { enabled: false, requireMention: "always" },
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type InboundPolicyDecision =
|
|
20
|
+
| { allow: true }
|
|
21
|
+
| { allow: false; reason: string; details?: Record<string, unknown> };
|
|
22
|
+
|
|
23
|
+
function mergePolicy(cfg: OmadeusChannelConfig | undefined) {
|
|
24
|
+
const inbound = cfg?.inbound;
|
|
25
|
+
const version = typeof inbound?.version === "number" && inbound.version >= 1 ? inbound.version : 1;
|
|
26
|
+
const direct = {
|
|
27
|
+
...DEFAULT_INBOUND_POLICY.direct,
|
|
28
|
+
...inbound?.direct,
|
|
29
|
+
requireMention: inbound?.direct?.requireMention ?? DEFAULT_INBOUND_POLICY.direct.requireMention,
|
|
30
|
+
};
|
|
31
|
+
const channels = {
|
|
32
|
+
...DEFAULT_INBOUND_POLICY.channels,
|
|
33
|
+
...inbound?.channels,
|
|
34
|
+
requireMention:
|
|
35
|
+
inbound?.channels?.requireMention ?? DEFAULT_INBOUND_POLICY.channels.requireMention,
|
|
36
|
+
};
|
|
37
|
+
const entities = {
|
|
38
|
+
...DEFAULT_INBOUND_POLICY.entities,
|
|
39
|
+
...inbound?.entities,
|
|
40
|
+
requireMention:
|
|
41
|
+
inbound?.entities?.requireMention ?? DEFAULT_INBOUND_POLICY.entities.requireMention,
|
|
42
|
+
};
|
|
43
|
+
return { version, direct, channels, entities };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function surfaceForKind(kind: OmadeusSubscribableKind): "direct" | "channel" | "entity" {
|
|
47
|
+
if (kind === "direct") return "direct";
|
|
48
|
+
if (kind === "channel") return "channel";
|
|
49
|
+
return "entity";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function senderAllowed(allowed: number[] | undefined, fromReferenceId: number): boolean {
|
|
53
|
+
if (!allowed || allowed.length === 0) return true;
|
|
54
|
+
return allowed.includes(fromReferenceId);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function channelGeoAllowed(params: {
|
|
58
|
+
roomId: number;
|
|
59
|
+
channelViewId?: number;
|
|
60
|
+
allowedRoomIds?: number[];
|
|
61
|
+
allowedChannelViewIds?: number[];
|
|
62
|
+
}): { geoInAllowlist: boolean; details: Record<string, unknown> } {
|
|
63
|
+
const { roomId, channelViewId, allowedRoomIds = [], allowedChannelViewIds = [] } = params;
|
|
64
|
+
const hasRooms = allowedRoomIds.length > 0;
|
|
65
|
+
const hasViews = allowedChannelViewIds.length > 0;
|
|
66
|
+
let geoInAllowlist = true;
|
|
67
|
+
if (hasRooms && hasViews) {
|
|
68
|
+
geoInAllowlist =
|
|
69
|
+
allowedRoomIds.includes(roomId) ||
|
|
70
|
+
(channelViewId !== undefined && allowedChannelViewIds.includes(channelViewId));
|
|
71
|
+
} else if (hasRooms) {
|
|
72
|
+
geoInAllowlist = allowedRoomIds.includes(roomId);
|
|
73
|
+
} else if (hasViews) {
|
|
74
|
+
geoInAllowlist = channelViewId !== undefined && allowedChannelViewIds.includes(channelViewId);
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
geoInAllowlist,
|
|
78
|
+
details: {
|
|
79
|
+
roomId,
|
|
80
|
+
channelViewId,
|
|
81
|
+
allowedRoomIds,
|
|
82
|
+
allowedChannelViewIds,
|
|
83
|
+
hasRooms,
|
|
84
|
+
hasViews,
|
|
85
|
+
geoInAllowlist,
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function entityKindAllowed(kind: OmadeusSubscribableKind, allowedKinds?: string[]): boolean {
|
|
91
|
+
if (!allowedKinds || allowedKinds.length === 0) {
|
|
92
|
+
return OMADEUS_INBOUND_ENTITY_KIND_SET.has(String(kind));
|
|
93
|
+
}
|
|
94
|
+
return allowedKinds.includes(String(kind));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function entityRoomOk(roomId: number, allowedRoomIds?: number[]): boolean {
|
|
98
|
+
if (!allowedRoomIds || allowedRoomIds.length === 0) return true;
|
|
99
|
+
return allowedRoomIds.includes(roomId);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function mentionRequired(params: {
|
|
103
|
+
requireMention?: "never" | "always" | "outsideAllowlist";
|
|
104
|
+
inAllowlist: boolean;
|
|
105
|
+
isMention: boolean;
|
|
106
|
+
}): boolean {
|
|
107
|
+
const requireMention = params.requireMention ?? "never";
|
|
108
|
+
const { inAllowlist, isMention } = params;
|
|
109
|
+
if (requireMention === "never") return false;
|
|
110
|
+
if (requireMention === "always") return !isMention;
|
|
111
|
+
// outsideAllowlist
|
|
112
|
+
if (inAllowlist) return false;
|
|
113
|
+
return !isMention;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Evaluate whether a normalized Jaguar inbound should be dispatched to OpenClaw.
|
|
118
|
+
* Callers must drop self-authored messages separately if they prefer logging there.
|
|
119
|
+
*/
|
|
120
|
+
export function evaluateOmadeusInboundPolicy(params: {
|
|
121
|
+
inbound: OmadeusInboundMessage;
|
|
122
|
+
omadeusCfg: OmadeusChannelConfig | undefined;
|
|
123
|
+
selfReferenceId: number;
|
|
124
|
+
}): InboundPolicyDecision {
|
|
125
|
+
const { inbound, omadeusCfg, selfReferenceId } = params;
|
|
126
|
+
|
|
127
|
+
if (inbound.fromReferenceId === selfReferenceId) {
|
|
128
|
+
return {
|
|
129
|
+
allow: false,
|
|
130
|
+
reason: "self_message",
|
|
131
|
+
details: { fromReferenceId: inbound.fromReferenceId, selfReferenceId },
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const policy = mergePolicy(omadeusCfg);
|
|
136
|
+
const surface = surfaceForKind(inbound.subscribableKind);
|
|
137
|
+
|
|
138
|
+
if (surface === "direct") {
|
|
139
|
+
if (!policy.direct.enabled) {
|
|
140
|
+
return { allow: false, reason: "direct_disabled", details: { surface } };
|
|
141
|
+
}
|
|
142
|
+
if (!senderAllowed(policy.direct.allowedSenderReferenceIds, inbound.fromReferenceId)) {
|
|
143
|
+
return {
|
|
144
|
+
allow: false,
|
|
145
|
+
reason: "direct_sender_not_allowed",
|
|
146
|
+
details: { fromReferenceId: inbound.fromReferenceId },
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
const req = policy.direct.requireMention ?? "never";
|
|
150
|
+
if (mentionRequired({ requireMention: req, inAllowlist: true, isMention: inbound.isMention })) {
|
|
151
|
+
return { allow: false, reason: "direct_mention_required", details: { requireMention: req } };
|
|
152
|
+
}
|
|
153
|
+
return { allow: true };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (surface === "channel") {
|
|
157
|
+
if (!policy.channels.enabled) {
|
|
158
|
+
return { allow: false, reason: "channels_disabled", details: { surface } };
|
|
159
|
+
}
|
|
160
|
+
if (!senderAllowed(policy.channels.allowedSenderReferenceIds, inbound.fromReferenceId)) {
|
|
161
|
+
return {
|
|
162
|
+
allow: false,
|
|
163
|
+
reason: "channel_sender_not_allowed",
|
|
164
|
+
details: { fromReferenceId: inbound.fromReferenceId },
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
const rv = channelGeoAllowed({
|
|
168
|
+
roomId: inbound.roomId,
|
|
169
|
+
channelViewId: inbound.channelViewId,
|
|
170
|
+
allowedRoomIds: policy.channels.allowedRoomIds,
|
|
171
|
+
allowedChannelViewIds: policy.channels.allowedChannelViewIds,
|
|
172
|
+
});
|
|
173
|
+
const senderInList =
|
|
174
|
+
!policy.channels.allowedSenderReferenceIds ||
|
|
175
|
+
policy.channels.allowedSenderReferenceIds.length === 0 ||
|
|
176
|
+
policy.channels.allowedSenderReferenceIds.includes(inbound.fromReferenceId);
|
|
177
|
+
const inAllowlist = rv.geoInAllowlist && senderInList;
|
|
178
|
+
const channelMention =
|
|
179
|
+
policy.channels.requireMention ?? DEFAULT_INBOUND_POLICY.channels.requireMention;
|
|
180
|
+
if (
|
|
181
|
+
mentionRequired({
|
|
182
|
+
requireMention: channelMention,
|
|
183
|
+
inAllowlist,
|
|
184
|
+
isMention: inbound.isMention,
|
|
185
|
+
})
|
|
186
|
+
) {
|
|
187
|
+
return {
|
|
188
|
+
allow: false,
|
|
189
|
+
reason: "channel_mention_required",
|
|
190
|
+
details: {
|
|
191
|
+
requireMention: channelMention,
|
|
192
|
+
inAllowlist,
|
|
193
|
+
isMention: inbound.isMention,
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
return { allow: true };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// entity
|
|
201
|
+
if (!policy.entities.enabled) {
|
|
202
|
+
return { allow: false, reason: "entities_disabled", details: { kind: inbound.subscribableKind } };
|
|
203
|
+
}
|
|
204
|
+
if (!entityKindAllowed(inbound.subscribableKind, policy.entities.allowedKinds)) {
|
|
205
|
+
return {
|
|
206
|
+
allow: false,
|
|
207
|
+
reason: "entity_kind_not_allowed",
|
|
208
|
+
details: { kind: inbound.subscribableKind, allowedKinds: policy.entities.allowedKinds },
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
if (!senderAllowed(policy.entities.allowedSenderReferenceIds, inbound.fromReferenceId)) {
|
|
212
|
+
return {
|
|
213
|
+
allow: false,
|
|
214
|
+
reason: "entity_sender_not_allowed",
|
|
215
|
+
details: { fromReferenceId: inbound.fromReferenceId },
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
if (!entityRoomOk(inbound.roomId, policy.entities.allowedRoomIds)) {
|
|
219
|
+
return {
|
|
220
|
+
allow: false,
|
|
221
|
+
reason: "entity_room_not_allowed",
|
|
222
|
+
details: { roomId: inbound.roomId, allowedRoomIds: policy.entities.allowedRoomIds },
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const roomList = policy.entities.allowedRoomIds ?? [];
|
|
227
|
+
const inAllowlist =
|
|
228
|
+
roomList.length === 0 || roomList.includes(inbound.roomId);
|
|
229
|
+
const entityMention =
|
|
230
|
+
policy.entities.requireMention ?? DEFAULT_INBOUND_POLICY.entities.requireMention;
|
|
231
|
+
if (
|
|
232
|
+
mentionRequired({
|
|
233
|
+
requireMention: entityMention,
|
|
234
|
+
inAllowlist,
|
|
235
|
+
isMention: inbound.isMention,
|
|
236
|
+
})
|
|
237
|
+
) {
|
|
238
|
+
return {
|
|
239
|
+
allow: false,
|
|
240
|
+
reason: "entity_mention_required",
|
|
241
|
+
details: {
|
|
242
|
+
requireMention: entityMention,
|
|
243
|
+
inAllowlist,
|
|
244
|
+
isMention: inbound.isMention,
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return { allow: true };
|
|
250
|
+
}
|
package/src/inbound.ts
CHANGED
|
@@ -56,6 +56,23 @@ function stripLeadingMention(body: string): string {
|
|
|
56
56
|
return body.replace(/^\*\*@[^*]+\*\*\s*/, "").trim();
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
function readChannelViewId(metadata: unknown): number | undefined {
|
|
60
|
+
if (metadata === null || metadata === undefined || typeof metadata !== "object") {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
const m = metadata as Record<string, unknown>;
|
|
64
|
+
for (const key of ["channelViewId", "channel_view_id", "viewId", "subscribableViewId"]) {
|
|
65
|
+
const v = m[key];
|
|
66
|
+
if (typeof v === "number" && Number.isFinite(v)) {
|
|
67
|
+
return v;
|
|
68
|
+
}
|
|
69
|
+
if (typeof v === "string" && /^\d+$/.test(v.trim())) {
|
|
70
|
+
return Number(v.trim());
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
59
76
|
/**
|
|
60
77
|
* Determine whether a raw Jaguar socket payload is an OmadeusMessage.
|
|
61
78
|
*/
|
|
@@ -95,6 +112,8 @@ export function parseJaguarMessage(
|
|
|
95
112
|
|
|
96
113
|
if (!content) return null;
|
|
97
114
|
|
|
115
|
+
const channelViewId = readChannelViewId(msg.metadata);
|
|
116
|
+
|
|
98
117
|
return {
|
|
99
118
|
messageId: msg.id,
|
|
100
119
|
from: String(msg.senderReferenceId),
|
|
@@ -104,6 +123,7 @@ export function parseJaguarMessage(
|
|
|
104
123
|
roomName: msg.roomName,
|
|
105
124
|
subscribableType: msg.subscribableType,
|
|
106
125
|
subscribableKind: msg.subscribableKind,
|
|
126
|
+
...(channelViewId !== undefined ? { channelViewId } : {}),
|
|
107
127
|
isMention: mentioned,
|
|
108
128
|
timestamp: msg.createdAtTimestamp
|
|
109
129
|
? Math.floor(msg.createdAtTimestamp * 1000)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { listOrganizationMembers } from "./api/auth.api.js";
|
|
2
|
+
import type { OmadeusOrganizationMember } from "./types.js";
|
|
3
|
+
import type { OmadeusApiOptions } from "./utils/http.util.js";
|
|
4
|
+
|
|
5
|
+
function formatMemberLabel(m: OmadeusOrganizationMember): string {
|
|
6
|
+
const fullName = `${m.firstName ?? ""} ${m.lastName ?? ""}`.trim();
|
|
7
|
+
if (fullName) {
|
|
8
|
+
return fullName;
|
|
9
|
+
}
|
|
10
|
+
if (m.title?.trim()) {
|
|
11
|
+
return m.title.trim();
|
|
12
|
+
}
|
|
13
|
+
if (m.email?.trim()) {
|
|
14
|
+
return m.email.trim();
|
|
15
|
+
}
|
|
16
|
+
return `Member ${m.referenceId}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Resolves Omadeus `referenceId` → human-readable label for the current organization (session JWT).
|
|
21
|
+
* Used so agents do not echo raw ids like 210 to users.
|
|
22
|
+
*/
|
|
23
|
+
export async function buildReferenceIdNameMap(
|
|
24
|
+
apiOpts: OmadeusApiOptions,
|
|
25
|
+
): Promise<Map<number, string>> {
|
|
26
|
+
const { organizationId } = apiOpts.tokenManager.getPayload();
|
|
27
|
+
const sessionToken = apiOpts.tokenManager.getToken();
|
|
28
|
+
const members = await listOrganizationMembers({
|
|
29
|
+
maestroUrl: apiOpts.maestroUrl,
|
|
30
|
+
sessionToken,
|
|
31
|
+
organizationId,
|
|
32
|
+
});
|
|
33
|
+
const map = new Map<number, string>();
|
|
34
|
+
for (const m of members) {
|
|
35
|
+
map.set(m.referenceId, formatMemberLabel(m));
|
|
36
|
+
}
|
|
37
|
+
return map;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Adds a `people` object mapping each `*ReferenceId` field in the nugget row to a display name.
|
|
42
|
+
* Keys match the source field names (e.g. `memberReferenceId: "Pat Example"`).
|
|
43
|
+
*/
|
|
44
|
+
export async function mergePeopleIntoNuggetAgentPayload(
|
|
45
|
+
apiOpts: OmadeusApiOptions,
|
|
46
|
+
fullRecord: Record<string, unknown>,
|
|
47
|
+
basePayload: Record<string, unknown>,
|
|
48
|
+
): Promise<Record<string, unknown>> {
|
|
49
|
+
let nameByRef: Map<number, string>;
|
|
50
|
+
try {
|
|
51
|
+
nameByRef = await buildReferenceIdNameMap(apiOpts);
|
|
52
|
+
} catch {
|
|
53
|
+
return { ...basePayload };
|
|
54
|
+
}
|
|
55
|
+
const people: Record<string, string> = {};
|
|
56
|
+
for (const [key, v] of Object.entries(fullRecord)) {
|
|
57
|
+
if (!/referenceid$/i.test(key)) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const id =
|
|
61
|
+
typeof v === "number" && Number.isFinite(v)
|
|
62
|
+
? v
|
|
63
|
+
: typeof v === "string" && /^\d+$/.test(v.trim())
|
|
64
|
+
? Number(v.trim())
|
|
65
|
+
: NaN;
|
|
66
|
+
if (!Number.isFinite(id)) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const name = nameByRef.get(id);
|
|
70
|
+
if (name) {
|
|
71
|
+
people[key] = name;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const out: Record<string, unknown> = { ...basePayload };
|
|
75
|
+
if (Object.keys(people).length > 0) {
|
|
76
|
+
out.people = people;
|
|
77
|
+
for (const key of Object.keys(people)) {
|
|
78
|
+
if (key in out) {
|
|
79
|
+
delete out[key];
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
package/src/message-handler.ts
CHANGED
|
@@ -5,8 +5,9 @@ import {
|
|
|
5
5
|
type OpenClawConfig,
|
|
6
6
|
type RuntimeEnv,
|
|
7
7
|
} from "../runtime-api.js";
|
|
8
|
-
import { createNugget, resolveTaskChannelRoomId, searchNuggetByNumber } from "./api/nugget.api.js";
|
|
8
|
+
import { createNugget, findNuggetByTaskChannelRoom, resolveTaskChannelRoomId, searchNuggetByNumber } from "./api/nugget.api.js";
|
|
9
9
|
import {
|
|
10
|
+
appendNuggetContextForTaskOrNuggetRoom,
|
|
10
11
|
appendNuggetLookupContextForAgent,
|
|
11
12
|
parseChannelTaskCreateIntent,
|
|
12
13
|
parseNuggetLookupIntent,
|
|
@@ -15,8 +16,12 @@ import {
|
|
|
15
16
|
import type { OutboundDeps } from "./outbound.js";
|
|
16
17
|
import { createOmadeusReplyDispatcher } from "./reply-dispatcher.js";
|
|
17
18
|
import { getOmadeusChannelConfig } from "./config.js";
|
|
19
|
+
import { evaluateOmadeusInboundPolicy } from "./inbound-policy.js";
|
|
18
20
|
import { getOmadeusRuntime } from "./runtime.js";
|
|
19
|
-
import
|
|
21
|
+
import {
|
|
22
|
+
type OmadeusInboundMessage,
|
|
23
|
+
OMADEUS_INBOUND_ENTITY_KIND_SET,
|
|
24
|
+
} from "./types.js";
|
|
20
25
|
|
|
21
26
|
type Log = {
|
|
22
27
|
info: (msg: string, extra?: Record<string, unknown>) => void;
|
|
@@ -25,15 +30,50 @@ type Log = {
|
|
|
25
30
|
debug?: (msg: string, extra?: Record<string, unknown>) => void;
|
|
26
31
|
};
|
|
27
32
|
|
|
33
|
+
const SK = "`subscribableKind`";
|
|
34
|
+
|
|
35
|
+
/** Injected into BodyForAgent so the model uses Jaguar room kind, not invented OpenClaw `task/...` keys. */
|
|
36
|
+
function buildOmadeusEntityRoomContextLine(kind: string): string {
|
|
37
|
+
if (kind === "task") {
|
|
38
|
+
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.`;
|
|
39
|
+
}
|
|
40
|
+
if (kind === "nugget") {
|
|
41
|
+
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}.)`;
|
|
42
|
+
}
|
|
43
|
+
const other: Record<string, string> = {
|
|
44
|
+
project: "Project",
|
|
45
|
+
release: "Release",
|
|
46
|
+
sprint: "Sprint",
|
|
47
|
+
summary: "Summary",
|
|
48
|
+
client: "Client",
|
|
49
|
+
folder: "Folder",
|
|
50
|
+
};
|
|
51
|
+
const label = other[kind] ?? kind;
|
|
52
|
+
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.`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function shouldSkipTaskRoomNuggetFetch(rawBody: string): boolean {
|
|
56
|
+
const t = rawBody.trim();
|
|
57
|
+
if (!t) {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
if (/^(ok|k|thanks?|thank you|ty|sounds good|got it|yes|yep|no|nope)\s*!*\.?$/i.test(t)) {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
28
66
|
export type OmadeusMessageHandlerDeps = {
|
|
29
67
|
cfg: OpenClawConfig;
|
|
30
68
|
runtime: RuntimeEnv;
|
|
31
69
|
log: Log;
|
|
32
70
|
outboundDeps: OutboundDeps;
|
|
71
|
+
/** Authenticated Omadeus user; used to drop self-authored messages and inbound policy. */
|
|
72
|
+
selfReferenceId: number;
|
|
33
73
|
};
|
|
34
74
|
|
|
35
75
|
export function createOmadeusMessageHandler(deps: OmadeusMessageHandlerDeps) {
|
|
36
|
-
const { cfg, runtime, log, outboundDeps } = deps;
|
|
76
|
+
const { cfg, runtime, log, outboundDeps, selfReferenceId } = deps;
|
|
37
77
|
const core = getOmadeusRuntime();
|
|
38
78
|
const omadeusCfg = getOmadeusChannelConfig(cfg);
|
|
39
79
|
|
|
@@ -54,55 +94,26 @@ export function createOmadeusMessageHandler(deps: OmadeusMessageHandlerDeps) {
|
|
|
54
94
|
return;
|
|
55
95
|
}
|
|
56
96
|
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
inbound.roomId === selectedPublicRoomId || inbound.roomId === selectedPrivateRoomId;
|
|
67
|
-
const isSelectedMember =
|
|
68
|
-
typeof selectedMemberReferenceId !== "number" ||
|
|
69
|
-
inbound.fromReferenceId === selectedMemberReferenceId;
|
|
70
|
-
isSelectedMemberTaskPrivate =
|
|
71
|
-
inbound.subscribableKind === "task" && inbound.isMention && isSelectedMember;
|
|
72
|
-
const allowSelectedChannelMessage = inSelectedChannelRoom && isSelectedMember;
|
|
73
|
-
if (!allowSelectedChannelMessage && !isSelectedMemberTaskPrivate) {
|
|
74
|
-
log.info("omadeus: dropped message outside selected channel scope", {
|
|
75
|
-
roomId: inbound.roomId,
|
|
76
|
-
roomName: inbound.roomName,
|
|
77
|
-
selectedPublicRoomId,
|
|
78
|
-
selectedPrivateRoomId,
|
|
79
|
-
selectedMemberReferenceId,
|
|
80
|
-
kind: inbound.subscribableKind,
|
|
81
|
-
fromReferenceId: inbound.fromReferenceId,
|
|
82
|
-
isMention: inbound.isMention,
|
|
83
|
-
selectedMemberMatched: isSelectedMember,
|
|
84
|
-
});
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const useAccessGroups =
|
|
90
|
-
(cfg.commands as Record<string, unknown> | undefined)?.useAccessGroups !== false;
|
|
91
|
-
|
|
92
|
-
// For group messages, only respond when mentioned (unless groupPolicy is open)
|
|
93
|
-
const bypassMentionGate = hasSelectedScope && (inSelectedChannelRoom || isSelectedMemberTaskPrivate);
|
|
94
|
-
if (!isDirectMessage && !inbound.isMention && !bypassMentionGate) {
|
|
95
|
-
log.debug?.("skipping group message (not mentioned)");
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
if (!isDirectMessage && !inbound.isMention && bypassMentionGate) {
|
|
99
|
-
log.info("omadeus: processing selected-scope group message without mention", {
|
|
97
|
+
const policyDecision = evaluateOmadeusInboundPolicy({
|
|
98
|
+
inbound,
|
|
99
|
+
omadeusCfg,
|
|
100
|
+
selfReferenceId,
|
|
101
|
+
});
|
|
102
|
+
if (!policyDecision.allow) {
|
|
103
|
+
log.info("omadeus: dropped message by inbound policy", {
|
|
104
|
+
reason: policyDecision.reason,
|
|
105
|
+
...(policyDecision.details ?? {}),
|
|
100
106
|
roomId: inbound.roomId,
|
|
101
|
-
roomName: inbound.roomName,
|
|
102
107
|
kind: inbound.subscribableKind,
|
|
108
|
+
fromReferenceId: inbound.fromReferenceId,
|
|
109
|
+
isMention: inbound.isMention,
|
|
103
110
|
});
|
|
111
|
+
return;
|
|
104
112
|
}
|
|
105
113
|
|
|
114
|
+
const useAccessGroups =
|
|
115
|
+
(cfg.commands as Record<string, unknown> | undefined)?.useAccessGroups !== false;
|
|
116
|
+
|
|
106
117
|
const hasControlCommand = core.channel.text.hasControlCommand(rawBody, cfg);
|
|
107
118
|
const commandGate = resolveControlCommandGate({
|
|
108
119
|
useAccessGroups,
|
|
@@ -125,10 +136,7 @@ export function createOmadeusMessageHandler(deps: OmadeusMessageHandlerDeps) {
|
|
|
125
136
|
const createIntent = parseChannelTaskCreateIntent(rawBody);
|
|
126
137
|
if (createIntent) {
|
|
127
138
|
try {
|
|
128
|
-
const memberReferenceId =
|
|
129
|
-
typeof selectedMemberReferenceId === "number"
|
|
130
|
-
? selectedMemberReferenceId
|
|
131
|
-
: inbound.fromReferenceId;
|
|
139
|
+
const memberReferenceId = inbound.fromReferenceId;
|
|
132
140
|
const created = await createNugget(outboundDeps.apiOpts, {
|
|
133
141
|
title: createIntent.title,
|
|
134
142
|
description: createIntent.description,
|
|
@@ -181,18 +189,49 @@ export function createOmadeusMessageHandler(deps: OmadeusMessageHandlerDeps) {
|
|
|
181
189
|
const nugget = await searchNuggetByNumber(outboundDeps.apiOpts, {
|
|
182
190
|
nuggetNumber: nuggetIntent.nuggetNumber,
|
|
183
191
|
});
|
|
184
|
-
bodyForAgent = appendNuggetLookupContextForAgent(
|
|
192
|
+
bodyForAgent = await appendNuggetLookupContextForAgent(
|
|
185
193
|
rawBody,
|
|
186
194
|
nuggetIntent.nuggetNumber,
|
|
187
195
|
nugget,
|
|
196
|
+
outboundDeps.apiOpts,
|
|
188
197
|
);
|
|
189
198
|
} catch (err) {
|
|
190
199
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
191
200
|
runtime.error?.(`omadeus nugget lookup failed: ${errorMessage}`);
|
|
192
|
-
bodyForAgent = appendNuggetLookupContextForAgent(
|
|
201
|
+
bodyForAgent = await appendNuggetLookupContextForAgent(
|
|
193
202
|
rawBody,
|
|
194
203
|
nuggetIntent.nuggetNumber,
|
|
195
204
|
null,
|
|
205
|
+
outboundDeps.apiOpts,
|
|
206
|
+
errorMessage,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const isTaskOrNuggetRoom =
|
|
212
|
+
!isDirectMessage && (inbound.subscribableKind === "task" || inbound.subscribableKind === "nugget");
|
|
213
|
+
if (isTaskOrNuggetRoom && !nuggetIntent && !createIntent && !shouldSkipTaskRoomNuggetFetch(rawBody)) {
|
|
214
|
+
try {
|
|
215
|
+
const nugget = await findNuggetByTaskChannelRoom(outboundDeps.apiOpts, {
|
|
216
|
+
roomId: inbound.roomId,
|
|
217
|
+
roomName: inbound.roomName,
|
|
218
|
+
});
|
|
219
|
+
bodyForAgent = await appendNuggetContextForTaskOrNuggetRoom(
|
|
220
|
+
bodyForAgent,
|
|
221
|
+
inbound.roomId,
|
|
222
|
+
inbound.roomName,
|
|
223
|
+
nugget,
|
|
224
|
+
outboundDeps.apiOpts,
|
|
225
|
+
);
|
|
226
|
+
} catch (err) {
|
|
227
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
228
|
+
runtime.error?.(`omadeus task room nugget lookup failed: ${errorMessage}`);
|
|
229
|
+
bodyForAgent = await appendNuggetContextForTaskOrNuggetRoom(
|
|
230
|
+
bodyForAgent,
|
|
231
|
+
inbound.roomId,
|
|
232
|
+
inbound.roomName,
|
|
233
|
+
null,
|
|
234
|
+
outboundDeps.apiOpts,
|
|
196
235
|
errorMessage,
|
|
197
236
|
);
|
|
198
237
|
}
|
|
@@ -210,6 +249,13 @@ export function createOmadeusMessageHandler(deps: OmadeusMessageHandlerDeps) {
|
|
|
210
249
|
},
|
|
211
250
|
});
|
|
212
251
|
|
|
252
|
+
// Omadeus entity rooms (Jaguar subscribableKind): see OmadeusInboundEntityKind,
|
|
253
|
+
// OMADEUS_INBOUND_ENTITY_KINDS. Models conflate "task" with OpenClaw and invent `task/<title>` keys.
|
|
254
|
+
if (!isDirectMessage && OMADEUS_INBOUND_ENTITY_KIND_SET.has(String(inbound.subscribableKind))) {
|
|
255
|
+
const entityLine = buildOmadeusEntityRoomContextLine(String(inbound.subscribableKind));
|
|
256
|
+
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.`;
|
|
257
|
+
}
|
|
258
|
+
|
|
213
259
|
const preview = rawBody.replace(/\s+/g, " ").slice(0, 160);
|
|
214
260
|
const inboundLabel = isDirectMessage
|
|
215
261
|
? `Omadeus DM from ${senderName}`
|