@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,626 @@
|
|
|
1
|
+
import { DEFAULT_ACCOUNT_ID, missingTargetError } from "../runtime-api.js";
|
|
2
|
+
import { ALLOWED_OMADEUS_REACTION_EMOJI_LIST, isAllowedOmadeusReactionEmoji } from "./allowed-reaction-emojis.js";
|
|
3
|
+
import { createNugget, resolveTaskRoomIdByNumber } from "./api/nugget.api.js";
|
|
4
|
+
import { addMessageReaction, deleteMessage, editMessage } from "./api/message.api.js";
|
|
5
|
+
import { getOmadeusChannelConfig, listOmadeusAccountIds, resolveDefaultOmadeusAccountId, resolveOmadeusAccount } from "./config.js";
|
|
6
|
+
import { parseJaguarMessage } from "./inbound.js";
|
|
7
|
+
import { parseTaskChannelTargetIntent } from "./nugget-lookup.js";
|
|
8
|
+
import { sendOmadeusMessage } from "./outbound.js";
|
|
9
|
+
import { getOmadeusRuntime } from "./runtime.js";
|
|
10
|
+
import { createOmadeusMessageHandler } from "./message-handler.js";
|
|
11
|
+
import { omadeusSetupAdapter } from "./setup-core.js";
|
|
12
|
+
import { omadeusSetupWizard } from "./onboarding.js";
|
|
13
|
+
import "./setup-surface.js";
|
|
14
|
+
import { createDolphinSocketClient } from "./socket/dolphin.socket.js";
|
|
15
|
+
import { createJaguarSocketClient } from "./socket/jaguar.socket.js";
|
|
16
|
+
import { createTokenManager } from "./token.js";
|
|
17
|
+
import { createTopLevelChannelConfigAdapter } from "openclaw/plugin-sdk/channel-config-helpers";
|
|
18
|
+
import { createAttachedChannelResultAdapter } from "openclaw/plugin-sdk/channel-send-result";
|
|
19
|
+
import { buildComputedAccountStatusSnapshot } from "openclaw/plugin-sdk/status-helpers";
|
|
20
|
+
import { buildPassiveChannelStatusSummary, buildTrafficStatusSummary } from "openclaw/plugin-sdk/extension-shared";
|
|
21
|
+
//#region src/channel.ts
|
|
22
|
+
const CHANNEL_ID = "omadeus";
|
|
23
|
+
const gatewayState = {
|
|
24
|
+
tokenManager: null,
|
|
25
|
+
dolphin: null,
|
|
26
|
+
jaguar: null
|
|
27
|
+
};
|
|
28
|
+
const isUnconfigured = (account) => account.credentialSource === "none";
|
|
29
|
+
let lastPersistedToken = null;
|
|
30
|
+
async function persistSessionToken(token) {
|
|
31
|
+
if (lastPersistedToken === token) return;
|
|
32
|
+
const runtime = getOmadeusRuntime();
|
|
33
|
+
if ((getOmadeusChannelConfig(runtime.config.current()) ?? {}).sessionToken === token) {
|
|
34
|
+
lastPersistedToken = token;
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
await runtime.config.mutateConfigFile({
|
|
38
|
+
afterWrite: { mode: "auto" },
|
|
39
|
+
mutate: (draft) => {
|
|
40
|
+
draft.channels = {
|
|
41
|
+
...draft.channels ?? {},
|
|
42
|
+
omadeus: {
|
|
43
|
+
...getOmadeusChannelConfig(draft) ?? {},
|
|
44
|
+
sessionToken: token
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
lastPersistedToken = token;
|
|
50
|
+
}
|
|
51
|
+
function actionError(text, error = text) {
|
|
52
|
+
return {
|
|
53
|
+
isError: true,
|
|
54
|
+
content: [{
|
|
55
|
+
type: "text",
|
|
56
|
+
text
|
|
57
|
+
}],
|
|
58
|
+
details: { error }
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function actionOk(payload) {
|
|
62
|
+
return {
|
|
63
|
+
content: [{
|
|
64
|
+
type: "text",
|
|
65
|
+
text: JSON.stringify({
|
|
66
|
+
ok: true,
|
|
67
|
+
channel: CHANNEL_ID,
|
|
68
|
+
...payload
|
|
69
|
+
})
|
|
70
|
+
}],
|
|
71
|
+
details: {
|
|
72
|
+
ok: true,
|
|
73
|
+
channel: CHANNEL_ID,
|
|
74
|
+
...payload
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
const omadeusConfigAdapter = createTopLevelChannelConfigAdapter({
|
|
79
|
+
sectionKey: "omadeus",
|
|
80
|
+
resolveAccount: (cfg) => resolveOmadeusAccount({ cfg }),
|
|
81
|
+
listAccountIds: listOmadeusAccountIds,
|
|
82
|
+
defaultAccountId: resolveDefaultOmadeusAccountId,
|
|
83
|
+
deleteMode: "clear-fields",
|
|
84
|
+
clearBaseFields: [
|
|
85
|
+
"casUrl",
|
|
86
|
+
"maestroUrl",
|
|
87
|
+
"email",
|
|
88
|
+
"password",
|
|
89
|
+
"organizationId",
|
|
90
|
+
"sessionToken",
|
|
91
|
+
"inbound"
|
|
92
|
+
],
|
|
93
|
+
resolveAllowFrom: () => [],
|
|
94
|
+
formatAllowFrom: () => []
|
|
95
|
+
});
|
|
96
|
+
const defaultRuntimeState = {
|
|
97
|
+
accountId: DEFAULT_ACCOUNT_ID,
|
|
98
|
+
running: false,
|
|
99
|
+
connected: false,
|
|
100
|
+
lastConnectedAt: null,
|
|
101
|
+
lastStartAt: null,
|
|
102
|
+
lastStopAt: null,
|
|
103
|
+
lastInboundAt: null,
|
|
104
|
+
lastOutboundAt: null,
|
|
105
|
+
lastError: null
|
|
106
|
+
};
|
|
107
|
+
/** Normalize Jaguar chat target: `room:123` or `123` -> `123` (numeric room id for APIs). */
|
|
108
|
+
function normalizeOmadeusRoomId(raw) {
|
|
109
|
+
const trimmed = raw.trim();
|
|
110
|
+
if (!trimmed) return;
|
|
111
|
+
const prefixed = /^room:(\d+)$/i.exec(trimmed);
|
|
112
|
+
if (prefixed) return prefixed[1];
|
|
113
|
+
return /^\d+$/.test(trimmed) ? trimmed : void 0;
|
|
114
|
+
}
|
|
115
|
+
function readReactionMessageId(params, toolContext) {
|
|
116
|
+
const raw = params.messageId ?? params.message_id ?? toolContext?.currentMessageId;
|
|
117
|
+
if (raw == null) return;
|
|
118
|
+
const n = typeof raw === "number" ? raw : Number(String(raw).trim());
|
|
119
|
+
return Number.isFinite(n) ? n : void 0;
|
|
120
|
+
}
|
|
121
|
+
function readStringParam(params, keys) {
|
|
122
|
+
for (const key of keys) {
|
|
123
|
+
const value = params[key];
|
|
124
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function readNumberParam(params, keys) {
|
|
128
|
+
for (const key of keys) {
|
|
129
|
+
const value = params[key];
|
|
130
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
131
|
+
if (typeof value === "string" && /^\d+$/.test(value.trim())) return Number(value.trim());
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function readNuggetKind(params) {
|
|
135
|
+
return readStringParam(params, [
|
|
136
|
+
"kind",
|
|
137
|
+
"entity",
|
|
138
|
+
"type"
|
|
139
|
+
])?.toLowerCase() === "nugget" ? "nugget" : "task";
|
|
140
|
+
}
|
|
141
|
+
function readNuggetPriority(params) {
|
|
142
|
+
const raw = readStringParam(params, ["priority"])?.toLowerCase();
|
|
143
|
+
if (raw === "urgent" || raw === "high" || raw === "medium" || raw === "low") return raw;
|
|
144
|
+
return "low";
|
|
145
|
+
}
|
|
146
|
+
function isCreateNuggetRequest(params) {
|
|
147
|
+
const op = readStringParam(params, [
|
|
148
|
+
"op",
|
|
149
|
+
"operation",
|
|
150
|
+
"intent"
|
|
151
|
+
])?.toLowerCase();
|
|
152
|
+
if (op === "create_nugget" || op === "create_task") return true;
|
|
153
|
+
if (params["createNugget"] === true || params["createTask"] === true || params["create"] === true || readStringParam(params, ["actionType", "mode"])?.toLowerCase() === "create") return true;
|
|
154
|
+
return Boolean(readStringParam(params, ["title"]) && readStringParam(params, ["description"]));
|
|
155
|
+
}
|
|
156
|
+
const omadeusPlugin = {
|
|
157
|
+
id: "omadeus",
|
|
158
|
+
meta: {
|
|
159
|
+
id: "omadeus",
|
|
160
|
+
label: "Omadeus",
|
|
161
|
+
selectionLabel: "Omadeus (API + WebSocket)",
|
|
162
|
+
docsPath: "",
|
|
163
|
+
docsLabel: "",
|
|
164
|
+
blurb: "AI-native project management that knows your role, speaks your language, and keeps your team in sync. No noise."
|
|
165
|
+
},
|
|
166
|
+
capabilities: {
|
|
167
|
+
chatTypes: ["direct", "group"],
|
|
168
|
+
reactions: true,
|
|
169
|
+
threads: false,
|
|
170
|
+
media: false,
|
|
171
|
+
nativeCommands: false,
|
|
172
|
+
blockStreaming: true
|
|
173
|
+
},
|
|
174
|
+
agentPrompt: { messageToolHints: () => [
|
|
175
|
+
"- Omadeus routing: **send** uses **room id** (`to` / `target`, e.g. `room:117947` or `117947`). **edit**, **delete**, **react** use the Jaguar **message** `id` (`messageId`, or the current inbound message from context).",
|
|
176
|
+
"- Create Omadeus task/nugget: use `action=send` with params `{ op: \"create_task\"|\"create_nugget\", title, description, priority?, stage?, kind?, memberReferenceId?, clientId?, folderId? }`.",
|
|
177
|
+
"- Omadeus **Task** and **Nugget** are distinct product types (Jaguar `subscribableKind`). **Project**, **Sprint**, **Release**, **Folder**, **Client**, **Summary**, etc. also have entity chat rooms. User \"task\" / \"the task\" → map to **this room's** `subscribableKind` (Task vs Nugget vs other), not an OpenClaw background task.",
|
|
178
|
+
"- In Task or Nugget rooms, inbound may include **Dolphin nuggetviews** JSON for this chat's `roomId` — **summarize that** for status questions. The payload may include a **`people`** object (Omadeus member names). Use those for assignees; do not read `referenceId` numbers as names. Do not tell the user to go use the Omadeus app instead of answering from that data or the thread.",
|
|
179
|
+
"- `session_status` / SessionKey: **OpenClaw** gateway only. Use the inbound SessionKey, \"current\", or the hint in **entity** rooms — never a fake `task/<...>` string from a title.",
|
|
180
|
+
`- Reactions only allow these emojis (others are ignored): ${ALLOWED_OMADEUS_REACTION_EMOJI_LIST.join(" ")}`,
|
|
181
|
+
"- Reply in chat with plain text; use the message tool for proactive sends, edits, deletes, or reactions."
|
|
182
|
+
] },
|
|
183
|
+
actions: {
|
|
184
|
+
describeMessageTool: ({ cfg }) => {
|
|
185
|
+
return {
|
|
186
|
+
actions: cfg.channels?.omadeus?.enabled !== false && !isUnconfigured(resolveOmadeusAccount({ cfg })) ? [
|
|
187
|
+
"send",
|
|
188
|
+
"edit",
|
|
189
|
+
"delete",
|
|
190
|
+
"react"
|
|
191
|
+
] : [],
|
|
192
|
+
capabilities: [],
|
|
193
|
+
schema: null
|
|
194
|
+
};
|
|
195
|
+
},
|
|
196
|
+
handleAction: async (ctx) => {
|
|
197
|
+
const account = resolveOmadeusAccount({ cfg: ctx.cfg });
|
|
198
|
+
const apiOpts = () => {
|
|
199
|
+
if (!gatewayState.tokenManager) throw new Error("Omadeus: not connected; gateway must be running with Omadeus enabled.");
|
|
200
|
+
return {
|
|
201
|
+
maestroUrl: account.maestroUrl,
|
|
202
|
+
tokenManager: gatewayState.tokenManager
|
|
203
|
+
};
|
|
204
|
+
};
|
|
205
|
+
if (ctx.action === "send" && isCreateNuggetRequest(ctx.params)) {
|
|
206
|
+
const title = readStringParam(ctx.params, [
|
|
207
|
+
"title",
|
|
208
|
+
"subject",
|
|
209
|
+
"name"
|
|
210
|
+
]);
|
|
211
|
+
const description = readStringParam(ctx.params, [
|
|
212
|
+
"description",
|
|
213
|
+
"details",
|
|
214
|
+
"body"
|
|
215
|
+
]);
|
|
216
|
+
if (!title || !description) return actionError("Omadeus create task/nugget requires `title` and `description`.", "Missing title/description.");
|
|
217
|
+
const kind = readNuggetKind(ctx.params);
|
|
218
|
+
const priority = readNuggetPriority(ctx.params);
|
|
219
|
+
const stage = readStringParam(ctx.params, ["stage"]) ?? "Triage";
|
|
220
|
+
const memberReferenceId = readNumberParam(ctx.params, ["memberReferenceId", "assigneeReferenceId"]) ?? gatewayState.tokenManager?.getPayload().referenceId;
|
|
221
|
+
const clientId = readNumberParam(ctx.params, ["clientId"]) ?? 1;
|
|
222
|
+
const folderId = readNumberParam(ctx.params, ["folderId"]) ?? 1;
|
|
223
|
+
if (!memberReferenceId) return actionError("Omadeus create task/nugget needs `memberReferenceId` or an active authenticated user.", "Missing memberReferenceId.");
|
|
224
|
+
try {
|
|
225
|
+
const created = await createNugget(apiOpts(), {
|
|
226
|
+
title,
|
|
227
|
+
description,
|
|
228
|
+
stage,
|
|
229
|
+
kind,
|
|
230
|
+
priority,
|
|
231
|
+
memberReferenceId,
|
|
232
|
+
clientId,
|
|
233
|
+
folderId
|
|
234
|
+
});
|
|
235
|
+
return actionOk({
|
|
236
|
+
action: "create",
|
|
237
|
+
kind,
|
|
238
|
+
number: created["number"],
|
|
239
|
+
id: created["id"],
|
|
240
|
+
title
|
|
241
|
+
});
|
|
242
|
+
} catch (err) {
|
|
243
|
+
return actionError(err instanceof Error ? err.message : String(err));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (ctx.action === "edit") {
|
|
247
|
+
const messageId = readReactionMessageId(ctx.params, ctx.toolContext);
|
|
248
|
+
const body = typeof ctx.params.message === "string" && ctx.params.message.trim() || typeof ctx.params.text === "string" && ctx.params.text.trim() || typeof ctx.params.content === "string" && ctx.params.content.trim() || "";
|
|
249
|
+
if (messageId == null) return actionError("Omadeus edit requires `messageId` (Jaguar message id) or current inbound MessageSid.", "Missing messageId for edit.");
|
|
250
|
+
if (!body) return actionError("Omadeus edit requires new text in `message`, `text`, or `content`.", "Missing body for edit.");
|
|
251
|
+
try {
|
|
252
|
+
await editMessage(apiOpts(), {
|
|
253
|
+
messageId,
|
|
254
|
+
body
|
|
255
|
+
});
|
|
256
|
+
} catch (err) {
|
|
257
|
+
return actionError(err instanceof Error ? err.message : String(err));
|
|
258
|
+
}
|
|
259
|
+
return actionOk({
|
|
260
|
+
action: "edit",
|
|
261
|
+
messageId
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
if (ctx.action === "delete") {
|
|
265
|
+
const messageId = readReactionMessageId(ctx.params, ctx.toolContext);
|
|
266
|
+
if (messageId == null) return actionError("Omadeus delete requires `messageId` (Jaguar message id) or current inbound MessageSid.", "Missing messageId for delete.");
|
|
267
|
+
try {
|
|
268
|
+
await deleteMessage(apiOpts(), { messageId });
|
|
269
|
+
} catch (err) {
|
|
270
|
+
return actionError(err instanceof Error ? err.message : String(err));
|
|
271
|
+
}
|
|
272
|
+
return actionOk({
|
|
273
|
+
action: "delete",
|
|
274
|
+
messageId
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
if (ctx.action === "react") {
|
|
278
|
+
const emoji = typeof ctx.params.emoji === "string" ? ctx.params.emoji.trim() : "";
|
|
279
|
+
if (!emoji) return actionError("Omadeus react requires `emoji`.", "Omadeus react requires emoji.");
|
|
280
|
+
if (!isAllowedOmadeusReactionEmoji(emoji)) return {
|
|
281
|
+
content: [{
|
|
282
|
+
type: "text",
|
|
283
|
+
text: JSON.stringify({
|
|
284
|
+
ok: true,
|
|
285
|
+
channel: CHANNEL_ID,
|
|
286
|
+
ignored: true,
|
|
287
|
+
reason: "unsupported_emoji",
|
|
288
|
+
emoji,
|
|
289
|
+
allowed: [...ALLOWED_OMADEUS_REACTION_EMOJI_LIST]
|
|
290
|
+
})
|
|
291
|
+
}],
|
|
292
|
+
details: {
|
|
293
|
+
ok: true,
|
|
294
|
+
ignored: true,
|
|
295
|
+
channel: CHANNEL_ID
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
const messageId = readReactionMessageId(ctx.params, ctx.toolContext);
|
|
299
|
+
if (messageId == null) return actionError("Omadeus react requires `messageId` or a current inbound message id (MessageSid).", "Missing messageId for reaction.");
|
|
300
|
+
try {
|
|
301
|
+
await addMessageReaction(apiOpts(), {
|
|
302
|
+
messageId,
|
|
303
|
+
emoji
|
|
304
|
+
});
|
|
305
|
+
} catch (err) {
|
|
306
|
+
return actionError(err instanceof Error ? err.message : String(err));
|
|
307
|
+
}
|
|
308
|
+
return actionOk({
|
|
309
|
+
messageId,
|
|
310
|
+
emoji
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
throw new Error(`Unhandled Omadeus action: ${String(ctx.action)}`);
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
reload: { configPrefixes: ["channels.omadeus"] },
|
|
317
|
+
setup: omadeusSetupAdapter,
|
|
318
|
+
setupWizard: omadeusSetupWizard,
|
|
319
|
+
config: {
|
|
320
|
+
...omadeusConfigAdapter,
|
|
321
|
+
isConfigured: (account) => !isUnconfigured(account),
|
|
322
|
+
unconfiguredReason: () => "Omadeus requires email, password, and organizationId. Run: openclaw setup omadeus",
|
|
323
|
+
describeAccount: (account) => ({
|
|
324
|
+
accountId: account.accountId,
|
|
325
|
+
name: account.name,
|
|
326
|
+
enabled: account.enabled,
|
|
327
|
+
configured: !isUnconfigured(account),
|
|
328
|
+
credentialSource: account.credentialSource,
|
|
329
|
+
baseUrl: account.maestroUrl
|
|
330
|
+
})
|
|
331
|
+
},
|
|
332
|
+
messaging: { targetResolver: {
|
|
333
|
+
hint: "Use room:<roomId> (matches OpenClaw OriginatingTo) or a numeric Jaguar room id.",
|
|
334
|
+
looksLikeId: (raw) => {
|
|
335
|
+
const t = raw.trim();
|
|
336
|
+
return /^room:\d+$/i.test(t) || /^\d+$/.test(t) || /^[nt]\d+$/i.test(t);
|
|
337
|
+
},
|
|
338
|
+
resolveTarget: async ({ cfg, input }) => {
|
|
339
|
+
const id = normalizeOmadeusRoomId(input);
|
|
340
|
+
if (!id) {
|
|
341
|
+
const taskIntent = parseTaskChannelTargetIntent(input);
|
|
342
|
+
if (!taskIntent || !gatewayState.tokenManager) return null;
|
|
343
|
+
const roomId = await resolveTaskRoomIdByNumber({
|
|
344
|
+
maestroUrl: resolveOmadeusAccount({ cfg }).maestroUrl,
|
|
345
|
+
tokenManager: gatewayState.tokenManager
|
|
346
|
+
}, { nuggetNumber: taskIntent.nuggetNumber });
|
|
347
|
+
if (!roomId) return null;
|
|
348
|
+
return {
|
|
349
|
+
to: String(roomId),
|
|
350
|
+
kind: "group",
|
|
351
|
+
display: `${taskIntent.rawPrefix.toUpperCase()}${taskIntent.nuggetNumber}`,
|
|
352
|
+
source: "normalized"
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
return {
|
|
356
|
+
to: id,
|
|
357
|
+
kind: "group",
|
|
358
|
+
display: `room:${id}`,
|
|
359
|
+
source: "normalized"
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
} },
|
|
363
|
+
outbound: {
|
|
364
|
+
deliveryMode: "direct",
|
|
365
|
+
textChunkLimit: 4e3,
|
|
366
|
+
chunker: (text, limit) => getOmadeusRuntime().channel.text.chunkMarkdownText(text, limit),
|
|
367
|
+
chunkerMode: "markdown",
|
|
368
|
+
...createAttachedChannelResultAdapter({
|
|
369
|
+
channel: CHANNEL_ID,
|
|
370
|
+
sendText: async ({ cfg, to, text }) => {
|
|
371
|
+
if (!gatewayState.jaguar || !gatewayState.tokenManager) throw new Error("Omadeus: not connected. Is the gateway running with Omadeus enabled?");
|
|
372
|
+
return await sendOmadeusMessage({
|
|
373
|
+
apiOpts: {
|
|
374
|
+
maestroUrl: resolveOmadeusAccount({ cfg }).maestroUrl,
|
|
375
|
+
tokenManager: gatewayState.tokenManager
|
|
376
|
+
},
|
|
377
|
+
jaguarSocket: gatewayState.jaguar
|
|
378
|
+
}, {
|
|
379
|
+
to,
|
|
380
|
+
text
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
}),
|
|
384
|
+
resolveTarget: ({ to }) => {
|
|
385
|
+
const trimmed = to?.trim() ?? "";
|
|
386
|
+
const id = normalizeOmadeusRoomId(trimmed);
|
|
387
|
+
if (!id) {
|
|
388
|
+
if (/^[nt]\d+$/i.test(trimmed)) return {
|
|
389
|
+
ok: true,
|
|
390
|
+
to: trimmed
|
|
391
|
+
};
|
|
392
|
+
return {
|
|
393
|
+
ok: false,
|
|
394
|
+
error: missingTargetError("Omadeus", "room:<roomId> or numeric room id")
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
return {
|
|
398
|
+
ok: true,
|
|
399
|
+
to: id
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
},
|
|
403
|
+
status: {
|
|
404
|
+
defaultRuntime: defaultRuntimeState,
|
|
405
|
+
collectStatusIssues: (accounts) => accounts.flatMap((entry) => {
|
|
406
|
+
const issues = [];
|
|
407
|
+
if (entry.enabled !== false && entry.configured !== true) issues.push({
|
|
408
|
+
channel: CHANNEL_ID,
|
|
409
|
+
accountId: String(entry.accountId ?? DEFAULT_ACCOUNT_ID),
|
|
410
|
+
kind: "config",
|
|
411
|
+
message: "Omadeus credentials are missing.",
|
|
412
|
+
fix: "Run: openclaw setup omadeus"
|
|
413
|
+
});
|
|
414
|
+
return issues;
|
|
415
|
+
}),
|
|
416
|
+
buildChannelSummary: ({ snapshot }) => ({
|
|
417
|
+
...buildPassiveChannelStatusSummary(snapshot, {
|
|
418
|
+
credentialSource: snapshot.credentialSource ?? "none",
|
|
419
|
+
baseUrl: snapshot.baseUrl ?? null,
|
|
420
|
+
connected: snapshot.connected ?? false,
|
|
421
|
+
lastConnectedAt: snapshot.lastConnectedAt ?? null
|
|
422
|
+
}),
|
|
423
|
+
...buildTrafficStatusSummary(snapshot)
|
|
424
|
+
}),
|
|
425
|
+
buildAccountSnapshot: ({ account, runtime }) => ({
|
|
426
|
+
...buildComputedAccountStatusSnapshot({
|
|
427
|
+
accountId: account.accountId,
|
|
428
|
+
name: account.name,
|
|
429
|
+
enabled: account.enabled,
|
|
430
|
+
configured: !isUnconfigured(account),
|
|
431
|
+
runtime
|
|
432
|
+
}),
|
|
433
|
+
baseUrl: account.maestroUrl,
|
|
434
|
+
credentialSource: account.credentialSource,
|
|
435
|
+
connected: runtime?.connected ?? false,
|
|
436
|
+
lastConnectedAt: runtime?.lastConnectedAt ?? null
|
|
437
|
+
})
|
|
438
|
+
},
|
|
439
|
+
gateway: { startAccount: async (ctx) => {
|
|
440
|
+
const { account, cfg, abortSignal } = ctx;
|
|
441
|
+
ctx.log?.info(`[omadeus] starting for org ${account.organizationId}`);
|
|
442
|
+
if (isUnconfigured(account)) {
|
|
443
|
+
ctx.log?.warn("[omadeus] skipping start: credentials not configured");
|
|
444
|
+
ctx.setStatus({
|
|
445
|
+
accountId: account.accountId,
|
|
446
|
+
running: false,
|
|
447
|
+
lastError: "credentials not configured"
|
|
448
|
+
});
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
const hasCachedSession = Boolean(account.sessionToken?.trim());
|
|
452
|
+
if (!account.password && !hasCachedSession) {
|
|
453
|
+
ctx.log?.warn("[omadeus] skipping start: password/sessionToken not set");
|
|
454
|
+
ctx.setStatus({
|
|
455
|
+
accountId: account.accountId,
|
|
456
|
+
running: false,
|
|
457
|
+
lastError: "password/sessionToken not set"
|
|
458
|
+
});
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
const log = ctx.log ?? {
|
|
462
|
+
info: () => {},
|
|
463
|
+
warn: () => {},
|
|
464
|
+
error: () => {}
|
|
465
|
+
};
|
|
466
|
+
let isConnected = false;
|
|
467
|
+
const tokenManager = createTokenManager({
|
|
468
|
+
casUrl: account.casUrl,
|
|
469
|
+
maestroUrl: account.maestroUrl,
|
|
470
|
+
email: account.email,
|
|
471
|
+
password: account.password,
|
|
472
|
+
organizationId: account.organizationId,
|
|
473
|
+
initialToken: account.sessionToken,
|
|
474
|
+
onRefresh: (token) => {
|
|
475
|
+
log.info("[omadeus] token refreshed");
|
|
476
|
+
persistSessionToken(token).catch((err) => log.warn(`[omadeus] failed to persist session token: ${String(err)}`));
|
|
477
|
+
},
|
|
478
|
+
onError: (err) => {
|
|
479
|
+
log.error(`[omadeus] token refresh failed: ${err.message}`);
|
|
480
|
+
ctx.setStatus({
|
|
481
|
+
accountId: account.accountId,
|
|
482
|
+
lastError: err.message
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
try {
|
|
487
|
+
await tokenManager.refresh();
|
|
488
|
+
} catch (err) {
|
|
489
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
490
|
+
log.error(`[omadeus] initial auth failed: ${msg}`);
|
|
491
|
+
ctx.setStatus({
|
|
492
|
+
accountId: account.accountId,
|
|
493
|
+
running: false,
|
|
494
|
+
lastError: msg
|
|
495
|
+
});
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
tokenManager.startAutoRefresh();
|
|
499
|
+
gatewayState.tokenManager = tokenManager;
|
|
500
|
+
const selfReferenceId = tokenManager.getPayload().referenceId;
|
|
501
|
+
const outboundDeps = {
|
|
502
|
+
apiOpts: {
|
|
503
|
+
maestroUrl: account.maestroUrl,
|
|
504
|
+
tokenManager
|
|
505
|
+
},
|
|
506
|
+
jaguarSocket: null
|
|
507
|
+
};
|
|
508
|
+
const handleMessage = createOmadeusMessageHandler({
|
|
509
|
+
cfg,
|
|
510
|
+
runtime: ctx.runtime,
|
|
511
|
+
log,
|
|
512
|
+
outboundDeps,
|
|
513
|
+
selfReferenceId
|
|
514
|
+
});
|
|
515
|
+
const jaguar = createJaguarSocketClient({
|
|
516
|
+
maestroUrl: account.maestroUrl,
|
|
517
|
+
tokenManager,
|
|
518
|
+
log,
|
|
519
|
+
onMessage: (msg) => {
|
|
520
|
+
const label = msg.subscribableKind === "direct" ? `DM from ${msg.senderReferenceId}` : `${msg.subscribableKind}/${msg.roomName ?? msg.roomId} from ${msg.senderReferenceId}`;
|
|
521
|
+
log.info(`[jaguar] ${label}: ${msg.body.slice(0, 80)}`);
|
|
522
|
+
const inbound = parseJaguarMessage(msg, { selfReferenceId }, log);
|
|
523
|
+
if (inbound) {
|
|
524
|
+
log.info(`[jaguar] inbound: ${inbound.subscribableKind} room=${inbound.roomId} from=${inbound.from} mention=${inbound.isMention}`);
|
|
525
|
+
ctx.setStatus({
|
|
526
|
+
accountId: account.accountId,
|
|
527
|
+
lastInboundAt: Date.now()
|
|
528
|
+
});
|
|
529
|
+
handleMessage(inbound).catch((err) => {
|
|
530
|
+
log.error(`[jaguar] dispatch error: ${err instanceof Error ? err.message : String(err)}`);
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
},
|
|
534
|
+
onOtherEvent: (data) => {
|
|
535
|
+
log.info(`[jaguar] non-message event: ${JSON.stringify(data).slice(0, 120)}`);
|
|
536
|
+
},
|
|
537
|
+
onConnect: () => {
|
|
538
|
+
if (!isConnected) {
|
|
539
|
+
isConnected = true;
|
|
540
|
+
ctx.setStatus({
|
|
541
|
+
accountId: account.accountId,
|
|
542
|
+
connected: true,
|
|
543
|
+
lastConnectedAt: Date.now()
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
},
|
|
547
|
+
onDisconnect: () => {
|
|
548
|
+
isConnected = false;
|
|
549
|
+
ctx.setStatus({
|
|
550
|
+
accountId: account.accountId,
|
|
551
|
+
connected: false
|
|
552
|
+
});
|
|
553
|
+
},
|
|
554
|
+
onError: (err) => ctx.setStatus({
|
|
555
|
+
accountId: account.accountId,
|
|
556
|
+
lastError: err.message
|
|
557
|
+
})
|
|
558
|
+
});
|
|
559
|
+
const dolphin = createDolphinSocketClient({
|
|
560
|
+
maestroUrl: account.maestroUrl,
|
|
561
|
+
tokenManager,
|
|
562
|
+
log,
|
|
563
|
+
onEvent: (data) => {
|
|
564
|
+
log.info(`[dolphin] event: ${JSON.stringify(data).slice(0, 120)}`);
|
|
565
|
+
},
|
|
566
|
+
onConnect: () => {
|
|
567
|
+
if (!isConnected) {
|
|
568
|
+
isConnected = true;
|
|
569
|
+
ctx.setStatus({
|
|
570
|
+
accountId: account.accountId,
|
|
571
|
+
connected: true,
|
|
572
|
+
lastConnectedAt: Date.now()
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
},
|
|
576
|
+
onDisconnect: () => {
|
|
577
|
+
isConnected = false;
|
|
578
|
+
ctx.setStatus({
|
|
579
|
+
accountId: account.accountId,
|
|
580
|
+
connected: false
|
|
581
|
+
});
|
|
582
|
+
},
|
|
583
|
+
onError: (err) => ctx.setStatus({
|
|
584
|
+
accountId: account.accountId,
|
|
585
|
+
lastError: err.message
|
|
586
|
+
})
|
|
587
|
+
});
|
|
588
|
+
outboundDeps.jaguarSocket = jaguar;
|
|
589
|
+
jaguar.connect();
|
|
590
|
+
dolphin.connect();
|
|
591
|
+
gatewayState.jaguar = jaguar;
|
|
592
|
+
gatewayState.dolphin = dolphin;
|
|
593
|
+
ctx.setStatus({
|
|
594
|
+
accountId: account.accountId,
|
|
595
|
+
running: true,
|
|
596
|
+
lastStartAt: Date.now()
|
|
597
|
+
});
|
|
598
|
+
let cleanedUp = false;
|
|
599
|
+
const cleanup = () => {
|
|
600
|
+
if (cleanedUp) return;
|
|
601
|
+
cleanedUp = true;
|
|
602
|
+
tokenManager.stopAutoRefresh();
|
|
603
|
+
jaguar.disconnect();
|
|
604
|
+
dolphin.disconnect();
|
|
605
|
+
gatewayState.tokenManager = null;
|
|
606
|
+
gatewayState.jaguar = null;
|
|
607
|
+
gatewayState.dolphin = null;
|
|
608
|
+
lastPersistedToken = null;
|
|
609
|
+
ctx.setStatus({
|
|
610
|
+
accountId: account.accountId,
|
|
611
|
+
running: false,
|
|
612
|
+
lastStopAt: Date.now()
|
|
613
|
+
});
|
|
614
|
+
};
|
|
615
|
+
await new Promise((resolve) => {
|
|
616
|
+
if (abortSignal.aborted) {
|
|
617
|
+
resolve();
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
abortSignal.addEventListener("abort", () => resolve(), { once: true });
|
|
621
|
+
});
|
|
622
|
+
cleanup();
|
|
623
|
+
} }
|
|
624
|
+
};
|
|
625
|
+
//#endregion
|
|
626
|
+
export { omadeusPlugin };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { DEFAULT_ACCOUNT_ID } from "../runtime-api.js";
|
|
2
|
+
import "./defaults.js";
|
|
3
|
+
//#region src/config.ts
|
|
4
|
+
function getOmadeusChannelConfig(cfg) {
|
|
5
|
+
return cfg.channels?.["omadeus"];
|
|
6
|
+
}
|
|
7
|
+
function listOmadeusAccountIds(cfg) {
|
|
8
|
+
if (!getOmadeusChannelConfig(cfg) && !resolveOmadeusEnvCredentials()) return [];
|
|
9
|
+
return [DEFAULT_ACCOUNT_ID];
|
|
10
|
+
}
|
|
11
|
+
function resolveDefaultOmadeusAccountId(_cfg) {
|
|
12
|
+
return DEFAULT_ACCOUNT_ID;
|
|
13
|
+
}
|
|
14
|
+
function resolveOmadeusAccount(params) {
|
|
15
|
+
const { cfg } = params;
|
|
16
|
+
const section = getOmadeusChannelConfig(cfg) ?? {};
|
|
17
|
+
const envCredentials = resolveOmadeusEnvCredentials();
|
|
18
|
+
const email = section.email?.trim() || envCredentials?.email || "";
|
|
19
|
+
const password = section.password?.trim() || envCredentials?.password || "";
|
|
20
|
+
const orgId = section.organizationId ?? envCredentials?.organizationId;
|
|
21
|
+
const sessionToken = section.sessionToken?.trim() ?? "";
|
|
22
|
+
const hasCredentials = Boolean(email && password && orgId);
|
|
23
|
+
const hasSessionToken = Boolean(sessionToken);
|
|
24
|
+
const credentialSource = Boolean(section.email?.trim() && section.password?.trim() && section.organizationId) ? "config" : hasCredentials ? "env" : hasSessionToken ? "session" : "none";
|
|
25
|
+
return {
|
|
26
|
+
accountId: DEFAULT_ACCOUNT_ID,
|
|
27
|
+
name: "Omadeus",
|
|
28
|
+
enabled: section.enabled !== false,
|
|
29
|
+
config: section,
|
|
30
|
+
casUrl: section.casUrl?.trim() || "https://dev1-cas.rouztech.com",
|
|
31
|
+
maestroUrl: section.maestroUrl?.trim() || "https://dev3-maestro.rouztech.com",
|
|
32
|
+
email,
|
|
33
|
+
password,
|
|
34
|
+
organizationId: orgId ?? 0,
|
|
35
|
+
...hasSessionToken ? { sessionToken } : {},
|
|
36
|
+
credentialSource
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function resolveOmadeusEnvCredentials() {
|
|
40
|
+
const email = process.env.OMADEUS_EMAIL?.trim();
|
|
41
|
+
const password = process.env.OMADEUS_PASSWORD?.trim();
|
|
42
|
+
const organizationIdRaw = process.env.OMADEUS_ORGANIZATION_ID?.trim();
|
|
43
|
+
if (!email || !password || !organizationIdRaw || !/^\d+$/.test(organizationIdRaw)) return;
|
|
44
|
+
return {
|
|
45
|
+
email,
|
|
46
|
+
password,
|
|
47
|
+
organizationId: Number(organizationIdRaw)
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** Whether messages from the authenticated user should be ignored. */
|
|
51
|
+
//#endregion
|
|
52
|
+
export { getOmadeusChannelConfig, listOmadeusAccountIds, resolveDefaultOmadeusAccountId, resolveOmadeusAccount };
|