@kittymi/openclaw-generic-http 0.1.3
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/LICENSE +21 -0
- package/README.md +132 -0
- package/dist/channel/account.d.ts +7 -0
- package/dist/channel/account.js +18 -0
- package/dist/channel/capabilities.d.ts +10 -0
- package/dist/channel/capabilities.js +11 -0
- package/dist/channel/host-adapter.d.ts +28 -0
- package/dist/channel/host-adapter.js +36 -0
- package/dist/channel/lifecycle.d.ts +18 -0
- package/dist/channel/lifecycle.js +28 -0
- package/dist/channel/plugin.d.ts +46 -0
- package/dist/channel/plugin.js +120 -0
- package/dist/channel/probe.d.ts +19 -0
- package/dist/channel/probe.js +149 -0
- package/dist/channel/resolve.d.ts +30 -0
- package/dist/channel/resolve.js +98 -0
- package/dist/channel/stream.d.ts +35 -0
- package/dist/channel/stream.js +127 -0
- package/dist/config/host-config-schema.d.ts +21 -0
- package/dist/config/host-config-schema.js +80 -0
- package/dist/config/loader.d.ts +7 -0
- package/dist/config/loader.js +38 -0
- package/dist/config/schema.d.ts +48 -0
- package/dist/config/schema.js +1 -0
- package/dist/errors/codes.d.ts +11 -0
- package/dist/errors/codes.js +10 -0
- package/dist/errors/exceptions.d.ts +7 -0
- package/dist/errors/exceptions.js +12 -0
- package/dist/inbound/mapper.d.ts +21 -0
- package/dist/inbound/mapper.js +22 -0
- package/dist/inbound/validator.d.ts +4 -0
- package/dist/inbound/validator.js +114 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +31 -0
- package/dist/mapping/conversation-mapper.d.ts +1 -0
- package/dist/mapping/conversation-mapper.js +3 -0
- package/dist/mapping/sender-mapper.d.ts +1 -0
- package/dist/mapping/sender-mapper.js +3 -0
- package/dist/mapping/thread-mapper.d.ts +1 -0
- package/dist/mapping/thread-mapper.js +7 -0
- package/dist/openclaw-entry.d.ts +276 -0
- package/dist/openclaw-entry.js +728 -0
- package/dist/outbound/client.d.ts +6 -0
- package/dist/outbound/client.js +1 -0
- package/dist/outbound/controller.d.ts +15 -0
- package/dist/outbound/controller.js +28 -0
- package/dist/outbound/http-client.d.ts +23 -0
- package/dist/outbound/http-client.js +150 -0
- package/dist/outbound/mapper.d.ts +29 -0
- package/dist/outbound/mapper.js +19 -0
- package/dist/outbound/mock-client.d.ts +18 -0
- package/dist/outbound/mock-client.js +26 -0
- package/dist/outbound/sender.d.ts +3 -0
- package/dist/outbound/sender.js +5 -0
- package/dist/protocol/attachments.d.ts +10 -0
- package/dist/protocol/attachments.js +56 -0
- package/dist/protocol/dto.d.ts +46 -0
- package/dist/protocol/dto.js +1 -0
- package/dist/protocol/serializer.d.ts +1 -0
- package/dist/protocol/serializer.js +3 -0
- package/dist/security/nonce-store.d.ts +30 -0
- package/dist/security/nonce-store.js +32 -0
- package/dist/security/signer.d.ts +10 -0
- package/dist/security/signer.js +20 -0
- package/dist/security/verifier.d.ts +2 -0
- package/dist/security/verifier.js +20 -0
- package/dist/setup-entry.d.ts +351 -0
- package/dist/setup-entry.js +73 -0
- package/dist/utils/json.d.ts +1 -0
- package/dist/utils/json.js +3 -0
- package/dist/utils/log.d.ts +1 -0
- package/dist/utils/log.js +3 -0
- package/dist/utils/time.d.ts +1 -0
- package/dist/utils/time.js +3 -0
- package/openclaw.config.schema.json +80 -0
- package/openclaw.plugin.json +175 -0
- package/package.json +72 -0
|
@@ -0,0 +1,728 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { createGenericHttpChannelLifecycle } from "./channel/lifecycle.js";
|
|
3
|
+
import { createGenericHttpChannelPlugin } from "./channel/plugin.js";
|
|
4
|
+
const CHANNEL_ID = "generic-http";
|
|
5
|
+
const CHANNEL_SECTION = "generic-http";
|
|
6
|
+
const DEFAULT_ACCOUNT_ID = "default";
|
|
7
|
+
const ROOT_THREAD_ID = "__root__";
|
|
8
|
+
function cloneConfig(value) {
|
|
9
|
+
return JSON.parse(JSON.stringify(value ?? {}));
|
|
10
|
+
}
|
|
11
|
+
function readChannelSection(cfg) {
|
|
12
|
+
const rawSection = cfg?.channels?.[CHANNEL_SECTION];
|
|
13
|
+
if (rawSection && typeof rawSection === "object" && !Array.isArray(rawSection)) {
|
|
14
|
+
return cloneConfig(rawSection);
|
|
15
|
+
}
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
function createRuntime(cfg) {
|
|
19
|
+
return createGenericHttpChannelPlugin(readChannelSection(cfg));
|
|
20
|
+
}
|
|
21
|
+
const gatewayLifecycles = new Map();
|
|
22
|
+
function resolveAccountSnapshot(cfg, accountId) {
|
|
23
|
+
const runtime = createRuntime(cfg);
|
|
24
|
+
const resolved = runtime.config;
|
|
25
|
+
const normalizedAccountId = typeof accountId === "string" && accountId.trim() !== ""
|
|
26
|
+
? accountId.trim()
|
|
27
|
+
: resolved.defaultAccount;
|
|
28
|
+
const account = resolved.accounts[normalizedAccountId];
|
|
29
|
+
return {
|
|
30
|
+
accountId: normalizedAccountId,
|
|
31
|
+
enabled: resolved.enabled,
|
|
32
|
+
name: normalizedAccountId === resolved.defaultAccount
|
|
33
|
+
? "Default account"
|
|
34
|
+
: normalizedAccountId,
|
|
35
|
+
configured: typeof account?.baseUrl === "string" && account.baseUrl.trim() !== "",
|
|
36
|
+
config: account ?? resolved.accounts[DEFAULT_ACCOUNT_ID]
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function chatTypeForConversationType(conversationType) {
|
|
40
|
+
if (conversationType === "group") {
|
|
41
|
+
return "group";
|
|
42
|
+
}
|
|
43
|
+
if (conversationType === "room" || conversationType === "ticket") {
|
|
44
|
+
return "channel";
|
|
45
|
+
}
|
|
46
|
+
return "direct";
|
|
47
|
+
}
|
|
48
|
+
function parseTarget(raw) {
|
|
49
|
+
const trimmed = raw.trim();
|
|
50
|
+
if (trimmed === "") {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
const strippedProviderPrefix = trimmed.replace(/^generic-http:/i, "").trim();
|
|
54
|
+
if (strippedProviderPrefix === "") {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
if (/^(dm|direct):/i.test(strippedProviderPrefix)) {
|
|
58
|
+
return {
|
|
59
|
+
conversationId: strippedProviderPrefix.replace(/^(dm|direct):/i, "").trim(),
|
|
60
|
+
conversationType: "dm",
|
|
61
|
+
chatType: "direct"
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
if (/^group:/i.test(strippedProviderPrefix)) {
|
|
65
|
+
return {
|
|
66
|
+
conversationId: strippedProviderPrefix.replace(/^group:/i, "").trim(),
|
|
67
|
+
conversationType: "group",
|
|
68
|
+
chatType: "group"
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
if (/^(channel|room):/i.test(strippedProviderPrefix)) {
|
|
72
|
+
return {
|
|
73
|
+
conversationId: strippedProviderPrefix.replace(/^(channel|room):/i, "").trim(),
|
|
74
|
+
conversationType: "room",
|
|
75
|
+
chatType: "channel"
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
conversationId: strippedProviderPrefix,
|
|
80
|
+
conversationType: "dm",
|
|
81
|
+
chatType: "direct"
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function normalizeTarget(raw) {
|
|
85
|
+
const parsed = parseTarget(raw);
|
|
86
|
+
if (!parsed || parsed.conversationId === "") {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
return `${parsed.chatType}:${parsed.conversationId}`;
|
|
90
|
+
}
|
|
91
|
+
function buildBaseSessionKey(params) {
|
|
92
|
+
return [
|
|
93
|
+
"agent",
|
|
94
|
+
params.agentId,
|
|
95
|
+
CHANNEL_ID,
|
|
96
|
+
params.accountId,
|
|
97
|
+
params.chatType,
|
|
98
|
+
params.conversationId
|
|
99
|
+
].join(":");
|
|
100
|
+
}
|
|
101
|
+
function normalizeSessionThreadId(threadId) {
|
|
102
|
+
if (threadId === null || threadId === undefined) {
|
|
103
|
+
return ROOT_THREAD_ID;
|
|
104
|
+
}
|
|
105
|
+
const normalized = String(threadId).trim();
|
|
106
|
+
return normalized === "" ? ROOT_THREAD_ID : normalized;
|
|
107
|
+
}
|
|
108
|
+
function toRoutePeer(conversationId, conversationType) {
|
|
109
|
+
return {
|
|
110
|
+
kind: chatTypeForConversationType(conversationType),
|
|
111
|
+
id: conversationId
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function toTargetRef(conversationId, conversationType) {
|
|
115
|
+
if (conversationType === "group") {
|
|
116
|
+
return `group:${conversationId}`;
|
|
117
|
+
}
|
|
118
|
+
if (conversationType === "room" || conversationType === "ticket") {
|
|
119
|
+
return `room:${conversationId}`;
|
|
120
|
+
}
|
|
121
|
+
return `dm:${conversationId}`;
|
|
122
|
+
}
|
|
123
|
+
function parseOccurredAtMillis(occurredAt) {
|
|
124
|
+
if (typeof occurredAt !== "string" || occurredAt.trim() === "") {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
const parsed = Date.parse(occurredAt);
|
|
128
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
129
|
+
}
|
|
130
|
+
function normalizeErrorMessage(error) {
|
|
131
|
+
if (error instanceof Error && error.message.trim() !== "") {
|
|
132
|
+
return error.message;
|
|
133
|
+
}
|
|
134
|
+
if (typeof error === "string" && error.trim() !== "") {
|
|
135
|
+
return error;
|
|
136
|
+
}
|
|
137
|
+
return "unknown generic-http gateway error";
|
|
138
|
+
}
|
|
139
|
+
function normalizeDisplayText(value) {
|
|
140
|
+
if (typeof value !== "string") {
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
const normalized = value.trim();
|
|
144
|
+
return normalized === "" ? undefined : normalized;
|
|
145
|
+
}
|
|
146
|
+
function buildConversationLabel(params) {
|
|
147
|
+
const title = normalizeDisplayText(params.conversationTitle);
|
|
148
|
+
const threadId = normalizeDisplayText(params.threadId);
|
|
149
|
+
const baseLabel = title ?? params.conversationId;
|
|
150
|
+
if (!threadId) {
|
|
151
|
+
return baseLabel;
|
|
152
|
+
}
|
|
153
|
+
return `${baseLabel} / ${threadId}`;
|
|
154
|
+
}
|
|
155
|
+
function requireChannelRuntime(value) {
|
|
156
|
+
if (value &&
|
|
157
|
+
typeof value === "object" &&
|
|
158
|
+
"routing" in value &&
|
|
159
|
+
"session" in value &&
|
|
160
|
+
"reply" in value &&
|
|
161
|
+
"turn" in value) {
|
|
162
|
+
return value;
|
|
163
|
+
}
|
|
164
|
+
throw new Error("OpenClaw channelRuntime is unavailable; generic-http stream ingress cannot dispatch inbound messages");
|
|
165
|
+
}
|
|
166
|
+
function finalizeInboundContextForRuntime(runtime, payload) {
|
|
167
|
+
if (typeof runtime.reply.finalizeInboundContext === "function") {
|
|
168
|
+
return runtime.reply.finalizeInboundContext(payload);
|
|
169
|
+
}
|
|
170
|
+
return payload;
|
|
171
|
+
}
|
|
172
|
+
async function deliverOutboundReply(params) {
|
|
173
|
+
const text = typeof params.payload.text === "string" ? params.payload.text : "";
|
|
174
|
+
const mediaUrls = [];
|
|
175
|
+
const rawMediaUrls = params.payload.mediaUrls;
|
|
176
|
+
if (Array.isArray(rawMediaUrls)) {
|
|
177
|
+
for (const mediaUrl of rawMediaUrls) {
|
|
178
|
+
if (typeof mediaUrl === "string" && mediaUrl.trim() !== "") {
|
|
179
|
+
mediaUrls.push(mediaUrl);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
else if (typeof params.payload.mediaUrl === "string" &&
|
|
184
|
+
params.payload.mediaUrl.trim() !== "") {
|
|
185
|
+
mediaUrls.push(params.payload.mediaUrl);
|
|
186
|
+
}
|
|
187
|
+
if (mediaUrls.length === 0) {
|
|
188
|
+
if (text.trim() === "") {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
await openClawGenericHttpChannelPlugin.outbound.sendText({
|
|
192
|
+
cfg: params.cfg,
|
|
193
|
+
to: toTargetRef(params.conversationId, params.conversationType),
|
|
194
|
+
text,
|
|
195
|
+
threadId: params.threadId,
|
|
196
|
+
accountId: params.accountId
|
|
197
|
+
});
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
for (const [index, mediaUrl] of mediaUrls.entries()) {
|
|
201
|
+
await openClawGenericHttpChannelPlugin.outbound.sendMedia({
|
|
202
|
+
cfg: params.cfg,
|
|
203
|
+
to: toTargetRef(params.conversationId, params.conversationType),
|
|
204
|
+
text: index === 0 ? text : "",
|
|
205
|
+
mediaUrl,
|
|
206
|
+
threadId: params.threadId,
|
|
207
|
+
accountId: params.accountId
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
async function dispatchInboundEventToOpenClaw(params) {
|
|
212
|
+
const runtime = requireChannelRuntime(params.ctx.channelRuntime);
|
|
213
|
+
const route = runtime.routing.resolveAgentRoute({
|
|
214
|
+
cfg: params.ctx.cfg,
|
|
215
|
+
channel: CHANNEL_ID,
|
|
216
|
+
accountId: params.event.accountId,
|
|
217
|
+
peer: toRoutePeer(params.event.conversationId, params.event.conversationType)
|
|
218
|
+
});
|
|
219
|
+
const sessionRoute = openClawGenericHttpChannelPlugin.messaging.resolveInboundSessionRoute({
|
|
220
|
+
agentId: route.agentId,
|
|
221
|
+
accountId: params.event.accountId,
|
|
222
|
+
conversationId: params.event.conversationId,
|
|
223
|
+
conversationType: params.event.conversationType,
|
|
224
|
+
threadId: params.event.threadId
|
|
225
|
+
});
|
|
226
|
+
const targetRef = toTargetRef(params.event.conversationId, params.event.conversationType);
|
|
227
|
+
const senderName = normalizeDisplayText(params.event.senderName) ?? params.event.senderId;
|
|
228
|
+
const conversationLabel = buildConversationLabel({
|
|
229
|
+
conversationId: params.event.conversationId,
|
|
230
|
+
conversationType: params.event.conversationType,
|
|
231
|
+
conversationTitle: params.event.conversationTitle,
|
|
232
|
+
threadId: params.event.threadId
|
|
233
|
+
});
|
|
234
|
+
const routeSessionKey = sessionRoute?.sessionKey ?? route.sessionKey;
|
|
235
|
+
const storePath = runtime.session.resolveStorePath(undefined, {
|
|
236
|
+
agentId: route.agentId
|
|
237
|
+
});
|
|
238
|
+
const chatType = chatTypeForConversationType(params.event.conversationType);
|
|
239
|
+
const groupSubject = chatType === "direct"
|
|
240
|
+
? undefined
|
|
241
|
+
: normalizeDisplayText(params.event.conversationTitle) ?? params.event.conversationId;
|
|
242
|
+
const inboundFrom = chatType === "direct" ? senderName : conversationLabel;
|
|
243
|
+
const ctxPayload = finalizeInboundContextForRuntime(runtime, {
|
|
244
|
+
Body: params.event.text ?? "",
|
|
245
|
+
BodyForAgent: params.event.text ?? "",
|
|
246
|
+
RawBody: params.event.text ?? "",
|
|
247
|
+
CommandBody: params.event.text ?? "",
|
|
248
|
+
From: inboundFrom,
|
|
249
|
+
To: targetRef,
|
|
250
|
+
SessionKey: routeSessionKey,
|
|
251
|
+
AccountId: params.event.accountId,
|
|
252
|
+
ChatType: chatType,
|
|
253
|
+
ConversationLabel: conversationLabel,
|
|
254
|
+
GroupSubject: groupSubject,
|
|
255
|
+
TopicName: normalizeDisplayText(params.event.threadId),
|
|
256
|
+
MessageThreadId: params.event.threadId ?? undefined,
|
|
257
|
+
SenderName: senderName,
|
|
258
|
+
SenderId: params.event.senderId,
|
|
259
|
+
Provider: CHANNEL_ID,
|
|
260
|
+
Surface: "stream",
|
|
261
|
+
MessageSid: params.event.messageId,
|
|
262
|
+
MessageSidFull: params.event.messageId,
|
|
263
|
+
ReplyToId: params.event.replyToMessageId ?? undefined,
|
|
264
|
+
Timestamp: parseOccurredAtMillis(params.event.occurredAt),
|
|
265
|
+
OriginatingChannel: CHANNEL_ID,
|
|
266
|
+
OriginatingTo: targetRef,
|
|
267
|
+
CommandAuthorized: false,
|
|
268
|
+
WasMentioned: chatType === "direct" ? undefined : true,
|
|
269
|
+
UntrustedStructuredContext: [
|
|
270
|
+
{
|
|
271
|
+
kind: "generic-http",
|
|
272
|
+
eventId: params.event.eventId,
|
|
273
|
+
idempotencyKey: params.event.idempotencyKey,
|
|
274
|
+
metadata: params.event.metadata
|
|
275
|
+
}
|
|
276
|
+
]
|
|
277
|
+
});
|
|
278
|
+
await runtime.turn.runAssembled({
|
|
279
|
+
cfg: params.ctx.cfg,
|
|
280
|
+
channel: CHANNEL_ID,
|
|
281
|
+
accountId: params.event.accountId,
|
|
282
|
+
agentId: route.agentId,
|
|
283
|
+
routeSessionKey,
|
|
284
|
+
storePath,
|
|
285
|
+
ctxPayload,
|
|
286
|
+
recordInboundSession: runtime.session.recordInboundSession,
|
|
287
|
+
dispatchReplyWithBufferedBlockDispatcher: runtime.reply.dispatchReplyWithBufferedBlockDispatcher,
|
|
288
|
+
delivery: {
|
|
289
|
+
deliver: async (payload) => {
|
|
290
|
+
await deliverOutboundReply({
|
|
291
|
+
cfg: params.ctx.cfg,
|
|
292
|
+
accountId: params.event.accountId,
|
|
293
|
+
conversationId: params.event.conversationId,
|
|
294
|
+
conversationType: params.event.conversationType,
|
|
295
|
+
threadId: params.event.threadId,
|
|
296
|
+
payload
|
|
297
|
+
});
|
|
298
|
+
return { visibleReplySent: true };
|
|
299
|
+
}
|
|
300
|
+
},
|
|
301
|
+
replyOptions: {
|
|
302
|
+
sourceReplyDeliveryMode: "automatic"
|
|
303
|
+
},
|
|
304
|
+
messageId: params.event.messageId
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
function inferAttachmentKind(mediaUrl) {
|
|
308
|
+
const pathname = new URL(mediaUrl).pathname.toLowerCase();
|
|
309
|
+
if (/\.(png|jpe?g|gif|webp|bmp|svg)$/.test(pathname)) {
|
|
310
|
+
return "image";
|
|
311
|
+
}
|
|
312
|
+
return "file";
|
|
313
|
+
}
|
|
314
|
+
function toAttachments(mediaUrl) {
|
|
315
|
+
if (typeof mediaUrl !== "string" || mediaUrl.trim() === "") {
|
|
316
|
+
return undefined;
|
|
317
|
+
}
|
|
318
|
+
return [
|
|
319
|
+
{
|
|
320
|
+
kind: inferAttachmentKind(mediaUrl),
|
|
321
|
+
url: mediaUrl
|
|
322
|
+
}
|
|
323
|
+
];
|
|
324
|
+
}
|
|
325
|
+
function nowRequestId() {
|
|
326
|
+
return randomUUID();
|
|
327
|
+
}
|
|
328
|
+
function buildOpenClawChannelPlugin() {
|
|
329
|
+
return {
|
|
330
|
+
id: CHANNEL_ID,
|
|
331
|
+
meta: {
|
|
332
|
+
id: CHANNEL_ID,
|
|
333
|
+
label: "Generic HTTP",
|
|
334
|
+
selectionLabel: "Generic HTTP",
|
|
335
|
+
docsPath: "/channels/generic-http",
|
|
336
|
+
blurb: "Bridge external systems into OpenClaw through webhook ingress and stream polling."
|
|
337
|
+
},
|
|
338
|
+
capabilities: {
|
|
339
|
+
chatTypes: ["direct", "group", "channel", "thread"],
|
|
340
|
+
media: true,
|
|
341
|
+
threads: true
|
|
342
|
+
},
|
|
343
|
+
reload: {
|
|
344
|
+
configPrefixes: ["channels.generic-http"]
|
|
345
|
+
},
|
|
346
|
+
configSchema: {
|
|
347
|
+
validate(value) {
|
|
348
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
349
|
+
return {
|
|
350
|
+
ok: false,
|
|
351
|
+
errors: ["channels.generic-http must be an object"]
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
return { ok: true, value: value };
|
|
355
|
+
}
|
|
356
|
+
},
|
|
357
|
+
config: {
|
|
358
|
+
listAccountIds(cfg) {
|
|
359
|
+
return createRuntime(cfg).status().accounts;
|
|
360
|
+
},
|
|
361
|
+
resolveAccount(cfg, accountId) {
|
|
362
|
+
return resolveAccountSnapshot(cfg, accountId);
|
|
363
|
+
},
|
|
364
|
+
defaultAccountId(cfg) {
|
|
365
|
+
return createRuntime(cfg).status().defaultAccount;
|
|
366
|
+
},
|
|
367
|
+
isEnabled(account) {
|
|
368
|
+
return account.enabled;
|
|
369
|
+
},
|
|
370
|
+
isConfigured(account) {
|
|
371
|
+
return account.configured;
|
|
372
|
+
},
|
|
373
|
+
describeAccount(account) {
|
|
374
|
+
return {
|
|
375
|
+
accountId: account.accountId,
|
|
376
|
+
name: account.name,
|
|
377
|
+
enabled: account.enabled,
|
|
378
|
+
configured: account.configured,
|
|
379
|
+
baseUrl: account.config.baseUrl
|
|
380
|
+
};
|
|
381
|
+
},
|
|
382
|
+
setAccountEnabled(params) {
|
|
383
|
+
const next = cloneConfig(params.cfg ?? {});
|
|
384
|
+
const channels = (next.channels ??= {});
|
|
385
|
+
const section = (channels[CHANNEL_SECTION] ??= {});
|
|
386
|
+
const accounts = (section.accounts ??= {});
|
|
387
|
+
const account = (accounts[params.accountId] ??= { baseUrl: "" });
|
|
388
|
+
section.enabled = params.enabled;
|
|
389
|
+
accounts[params.accountId] = account;
|
|
390
|
+
section.defaultAccount = section.defaultAccount ?? params.accountId;
|
|
391
|
+
return next;
|
|
392
|
+
}
|
|
393
|
+
},
|
|
394
|
+
setup: {
|
|
395
|
+
resolveAccountId(params) {
|
|
396
|
+
if (typeof params.accountId === "string" && params.accountId.trim() !== "") {
|
|
397
|
+
return params.accountId.trim();
|
|
398
|
+
}
|
|
399
|
+
return createRuntime(params.cfg).status().defaultAccount;
|
|
400
|
+
},
|
|
401
|
+
validateInput(params) {
|
|
402
|
+
const baseUrl = params.input.baseUrl ?? params.input.url;
|
|
403
|
+
if (typeof baseUrl !== "string" || baseUrl.trim() === "") {
|
|
404
|
+
return "baseUrl is required";
|
|
405
|
+
}
|
|
406
|
+
try {
|
|
407
|
+
new URL(baseUrl);
|
|
408
|
+
}
|
|
409
|
+
catch {
|
|
410
|
+
return "baseUrl must be a valid absolute URL";
|
|
411
|
+
}
|
|
412
|
+
return null;
|
|
413
|
+
},
|
|
414
|
+
applyAccountConfig(params) {
|
|
415
|
+
const next = cloneConfig(params.cfg ?? {});
|
|
416
|
+
const channels = (next.channels ??= {});
|
|
417
|
+
const section = (channels[CHANNEL_SECTION] ??= {});
|
|
418
|
+
const accounts = (section.accounts ??= {});
|
|
419
|
+
const previous = accounts[params.accountId] ?? { baseUrl: "" };
|
|
420
|
+
const baseUrl = params.input.baseUrl ?? params.input.url ?? previous.baseUrl ?? "";
|
|
421
|
+
accounts[params.accountId] = {
|
|
422
|
+
...previous,
|
|
423
|
+
baseUrl,
|
|
424
|
+
apiKey: params.input.token ?? previous.apiKey,
|
|
425
|
+
signingSecret: params.input.secret ?? previous.signingSecret
|
|
426
|
+
};
|
|
427
|
+
section.enabled = true;
|
|
428
|
+
section.defaultAccount = section.defaultAccount ?? params.accountId;
|
|
429
|
+
return next;
|
|
430
|
+
}
|
|
431
|
+
},
|
|
432
|
+
status: {
|
|
433
|
+
defaultRuntime: {
|
|
434
|
+
accountId: DEFAULT_ACCOUNT_ID,
|
|
435
|
+
running: false,
|
|
436
|
+
connected: false,
|
|
437
|
+
lastStartAt: null,
|
|
438
|
+
lastStopAt: null,
|
|
439
|
+
lastError: null,
|
|
440
|
+
lastInboundAt: null,
|
|
441
|
+
lastOutboundAt: null,
|
|
442
|
+
lastTransportActivityAt: null
|
|
443
|
+
},
|
|
444
|
+
async probeAccount(params) {
|
|
445
|
+
return await createRuntime(params.cfg).probe(params.account.accountId);
|
|
446
|
+
},
|
|
447
|
+
buildAccountSnapshot(params) {
|
|
448
|
+
const runtime = params.runtime;
|
|
449
|
+
return {
|
|
450
|
+
accountId: params.account.accountId,
|
|
451
|
+
name: params.account.name,
|
|
452
|
+
enabled: params.account.enabled,
|
|
453
|
+
configured: params.account.configured,
|
|
454
|
+
baseUrl: params.account.config.baseUrl,
|
|
455
|
+
running: runtime?.running ?? false,
|
|
456
|
+
connected: runtime?.connected ?? false,
|
|
457
|
+
lastStartAt: runtime?.lastStartAt ?? null,
|
|
458
|
+
lastStopAt: runtime?.lastStopAt ?? null,
|
|
459
|
+
lastError: runtime?.lastError ?? null,
|
|
460
|
+
lastInboundAt: runtime?.lastInboundAt ?? null,
|
|
461
|
+
lastOutboundAt: runtime?.lastOutboundAt ?? null,
|
|
462
|
+
lastTransportActivityAt: runtime?.lastTransportActivityAt ?? null,
|
|
463
|
+
probe: params.probe,
|
|
464
|
+
lastProbeAt: Date.now()
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
},
|
|
468
|
+
gateway: {
|
|
469
|
+
async startAccount(ctx) {
|
|
470
|
+
const existing = gatewayLifecycles.get(ctx.accountId);
|
|
471
|
+
existing?.stop();
|
|
472
|
+
existing?.close();
|
|
473
|
+
const lifecycle = createGenericHttpChannelLifecycle(readChannelSection(ctx.cfg), {
|
|
474
|
+
async dispatchInboundEvent(event) {
|
|
475
|
+
const activityAt = parseOccurredAtMillis(event.occurredAt) ?? Date.now();
|
|
476
|
+
ctx.setStatus({
|
|
477
|
+
accountId: ctx.accountId,
|
|
478
|
+
connected: true,
|
|
479
|
+
lastInboundAt: activityAt,
|
|
480
|
+
lastTransportActivityAt: Date.now(),
|
|
481
|
+
lastError: null
|
|
482
|
+
});
|
|
483
|
+
await dispatchInboundEventToOpenClaw({ ctx, event });
|
|
484
|
+
},
|
|
485
|
+
async onStreamError(error) {
|
|
486
|
+
const message = normalizeErrorMessage(error);
|
|
487
|
+
ctx.log?.error?.(`[${ctx.accountId}] ${message}`);
|
|
488
|
+
ctx.setStatus({
|
|
489
|
+
accountId: ctx.accountId,
|
|
490
|
+
connected: false,
|
|
491
|
+
lastError: message
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
gatewayLifecycles.set(ctx.accountId, lifecycle);
|
|
496
|
+
ctx.log?.info?.(`[${ctx.accountId}] starting generic-http stream ingress`);
|
|
497
|
+
ctx.setStatus({
|
|
498
|
+
accountId: ctx.accountId,
|
|
499
|
+
running: true,
|
|
500
|
+
connected: true,
|
|
501
|
+
lastStartAt: Date.now(),
|
|
502
|
+
lastError: null
|
|
503
|
+
});
|
|
504
|
+
try {
|
|
505
|
+
await lifecycle.start(ctx.accountId);
|
|
506
|
+
await new Promise((resolve) => {
|
|
507
|
+
if (ctx.abortSignal.aborted) {
|
|
508
|
+
resolve();
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
ctx.abortSignal.addEventListener("abort", () => resolve(), {
|
|
512
|
+
once: true
|
|
513
|
+
});
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
finally {
|
|
517
|
+
lifecycle.stop();
|
|
518
|
+
lifecycle.close();
|
|
519
|
+
gatewayLifecycles.delete(ctx.accountId);
|
|
520
|
+
ctx.setStatus({
|
|
521
|
+
accountId: ctx.accountId,
|
|
522
|
+
running: false,
|
|
523
|
+
connected: false,
|
|
524
|
+
lastStopAt: Date.now()
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
},
|
|
528
|
+
async stopAccount(ctx) {
|
|
529
|
+
const lifecycle = gatewayLifecycles.get(ctx.accountId);
|
|
530
|
+
if (!lifecycle) {
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
lifecycle.stop();
|
|
534
|
+
lifecycle.close();
|
|
535
|
+
gatewayLifecycles.delete(ctx.accountId);
|
|
536
|
+
ctx.setStatus({
|
|
537
|
+
accountId: ctx.accountId,
|
|
538
|
+
running: false,
|
|
539
|
+
connected: false,
|
|
540
|
+
lastStopAt: Date.now()
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
},
|
|
544
|
+
resolver: {
|
|
545
|
+
async resolveTargets(params) {
|
|
546
|
+
const runtime = createRuntime(params.cfg);
|
|
547
|
+
return await Promise.all(params.inputs.map(async (input) => {
|
|
548
|
+
const response = await runtime.resolve({
|
|
549
|
+
accountId: params.accountId,
|
|
550
|
+
kind: params.kind === "user" ? "sender" : "conversation",
|
|
551
|
+
query: input
|
|
552
|
+
});
|
|
553
|
+
const first = response.results[0];
|
|
554
|
+
if (!first) {
|
|
555
|
+
return {
|
|
556
|
+
input,
|
|
557
|
+
resolved: false,
|
|
558
|
+
note: "No match returned by remote resolve endpoint"
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
return {
|
|
562
|
+
input,
|
|
563
|
+
resolved: true,
|
|
564
|
+
id: first.id,
|
|
565
|
+
name: first.name
|
|
566
|
+
};
|
|
567
|
+
}));
|
|
568
|
+
}
|
|
569
|
+
},
|
|
570
|
+
messaging: {
|
|
571
|
+
targetPrefixes: ["generic-http", "gh"],
|
|
572
|
+
normalizeTarget,
|
|
573
|
+
parseExplicitTarget(params) {
|
|
574
|
+
const parsed = parseTarget(params.raw);
|
|
575
|
+
if (!parsed) {
|
|
576
|
+
return null;
|
|
577
|
+
}
|
|
578
|
+
return {
|
|
579
|
+
to: parsed.conversationId,
|
|
580
|
+
chatType: parsed.chatType
|
|
581
|
+
};
|
|
582
|
+
},
|
|
583
|
+
inferTargetChatType(params) {
|
|
584
|
+
const parsed = parseTarget(params.to);
|
|
585
|
+
return parsed?.chatType;
|
|
586
|
+
},
|
|
587
|
+
resolveOutboundSessionRoute(params) {
|
|
588
|
+
const parsed = parseTarget(params.target);
|
|
589
|
+
if (!parsed) {
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
const accountId = typeof params.accountId === "string" && params.accountId.trim() !== ""
|
|
593
|
+
? params.accountId.trim()
|
|
594
|
+
: DEFAULT_ACCOUNT_ID;
|
|
595
|
+
const baseSessionKey = buildBaseSessionKey({
|
|
596
|
+
agentId: params.agentId,
|
|
597
|
+
accountId,
|
|
598
|
+
chatType: parsed.chatType,
|
|
599
|
+
conversationId: parsed.conversationId
|
|
600
|
+
});
|
|
601
|
+
const normalizedThreadId = normalizeSessionThreadId(params.threadId);
|
|
602
|
+
return {
|
|
603
|
+
sessionKey: `${baseSessionKey}:thread:${normalizedThreadId}`,
|
|
604
|
+
baseSessionKey,
|
|
605
|
+
peer: {
|
|
606
|
+
kind: parsed.chatType,
|
|
607
|
+
id: parsed.conversationId
|
|
608
|
+
},
|
|
609
|
+
chatType: parsed.chatType,
|
|
610
|
+
from: `${CHANNEL_ID}:${parsed.conversationId}`,
|
|
611
|
+
to: `${CHANNEL_ID}:${parsed.conversationId}`,
|
|
612
|
+
threadId: normalizedThreadId
|
|
613
|
+
};
|
|
614
|
+
},
|
|
615
|
+
resolveInboundSessionRoute(params) {
|
|
616
|
+
const normalizedConversationId = params.conversationId.trim();
|
|
617
|
+
if (normalizedConversationId === "") {
|
|
618
|
+
return null;
|
|
619
|
+
}
|
|
620
|
+
const accountId = typeof params.accountId === "string" && params.accountId.trim() !== ""
|
|
621
|
+
? params.accountId.trim()
|
|
622
|
+
: DEFAULT_ACCOUNT_ID;
|
|
623
|
+
const chatType = chatTypeForConversationType(params.conversationType);
|
|
624
|
+
const baseSessionKey = buildBaseSessionKey({
|
|
625
|
+
agentId: params.agentId,
|
|
626
|
+
accountId,
|
|
627
|
+
chatType,
|
|
628
|
+
conversationId: normalizedConversationId
|
|
629
|
+
});
|
|
630
|
+
const normalizedThreadId = normalizeSessionThreadId(params.threadId);
|
|
631
|
+
return {
|
|
632
|
+
sessionKey: `${baseSessionKey}:thread:${normalizedThreadId}`,
|
|
633
|
+
baseSessionKey,
|
|
634
|
+
peer: {
|
|
635
|
+
kind: chatType,
|
|
636
|
+
id: normalizedConversationId
|
|
637
|
+
},
|
|
638
|
+
chatType,
|
|
639
|
+
from: `${CHANNEL_ID}:${normalizedConversationId}`,
|
|
640
|
+
to: `${CHANNEL_ID}:${normalizedConversationId}`,
|
|
641
|
+
threadId: normalizedThreadId
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
},
|
|
645
|
+
outbound: {
|
|
646
|
+
deliveryMode: "direct",
|
|
647
|
+
async sendText(ctx) {
|
|
648
|
+
const parsed = parseTarget(ctx.to);
|
|
649
|
+
if (!parsed) {
|
|
650
|
+
throw new Error("generic-http target is required");
|
|
651
|
+
}
|
|
652
|
+
const runtime = createRuntime(ctx.cfg);
|
|
653
|
+
const result = await runtime.sendOutboundMessage({
|
|
654
|
+
requestId: nowRequestId(),
|
|
655
|
+
accountId: ctx.accountId ?? DEFAULT_ACCOUNT_ID,
|
|
656
|
+
conversationId: parsed.conversationId,
|
|
657
|
+
conversationType: parsed.conversationType,
|
|
658
|
+
threadId: ctx.threadId === null || ctx.threadId === undefined
|
|
659
|
+
? null
|
|
660
|
+
: String(ctx.threadId),
|
|
661
|
+
messageId: nowRequestId(),
|
|
662
|
+
text: ctx.text
|
|
663
|
+
});
|
|
664
|
+
return {
|
|
665
|
+
channel: CHANNEL_ID,
|
|
666
|
+
messageId: result.providerMessageId,
|
|
667
|
+
conversationId: parsed.conversationId,
|
|
668
|
+
timestamp: Date.parse(result.acceptedAt),
|
|
669
|
+
meta: result.metadata
|
|
670
|
+
};
|
|
671
|
+
},
|
|
672
|
+
async sendMedia(ctx) {
|
|
673
|
+
const parsed = parseTarget(ctx.to);
|
|
674
|
+
if (!parsed) {
|
|
675
|
+
throw new Error("generic-http target is required");
|
|
676
|
+
}
|
|
677
|
+
const runtime = createRuntime(ctx.cfg);
|
|
678
|
+
const result = await runtime.sendOutboundMessage({
|
|
679
|
+
requestId: nowRequestId(),
|
|
680
|
+
accountId: ctx.accountId ?? DEFAULT_ACCOUNT_ID,
|
|
681
|
+
conversationId: parsed.conversationId,
|
|
682
|
+
conversationType: parsed.conversationType,
|
|
683
|
+
threadId: ctx.threadId === null || ctx.threadId === undefined
|
|
684
|
+
? null
|
|
685
|
+
: String(ctx.threadId),
|
|
686
|
+
messageId: nowRequestId(),
|
|
687
|
+
text: ctx.text,
|
|
688
|
+
attachments: toAttachments(ctx.mediaUrl)
|
|
689
|
+
});
|
|
690
|
+
return {
|
|
691
|
+
channel: CHANNEL_ID,
|
|
692
|
+
messageId: result.providerMessageId,
|
|
693
|
+
conversationId: parsed.conversationId,
|
|
694
|
+
timestamp: Date.parse(result.acceptedAt),
|
|
695
|
+
meta: result.metadata
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
export const openClawGenericHttpChannelPlugin = buildOpenClawChannelPlugin();
|
|
702
|
+
export const openClawGenericHttpPluginEntry = {
|
|
703
|
+
id: "openclaw-generic-http",
|
|
704
|
+
name: "Generic HTTP",
|
|
705
|
+
description: "Generic HTTP channel plugin for OpenClaw",
|
|
706
|
+
configSchema: {
|
|
707
|
+
validate(value) {
|
|
708
|
+
if (value === undefined) {
|
|
709
|
+
return { ok: true, value: {} };
|
|
710
|
+
}
|
|
711
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
712
|
+
return {
|
|
713
|
+
ok: false,
|
|
714
|
+
errors: ["plugin config must be an object"]
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
return { ok: true, value: value };
|
|
718
|
+
}
|
|
719
|
+
},
|
|
720
|
+
register(api) {
|
|
721
|
+
if (api.registrationMode === "cli-metadata") {
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
api.registerChannel({
|
|
725
|
+
plugin: openClawGenericHttpChannelPlugin
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
};
|