@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.
Files changed (77) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +132 -0
  3. package/dist/channel/account.d.ts +7 -0
  4. package/dist/channel/account.js +18 -0
  5. package/dist/channel/capabilities.d.ts +10 -0
  6. package/dist/channel/capabilities.js +11 -0
  7. package/dist/channel/host-adapter.d.ts +28 -0
  8. package/dist/channel/host-adapter.js +36 -0
  9. package/dist/channel/lifecycle.d.ts +18 -0
  10. package/dist/channel/lifecycle.js +28 -0
  11. package/dist/channel/plugin.d.ts +46 -0
  12. package/dist/channel/plugin.js +120 -0
  13. package/dist/channel/probe.d.ts +19 -0
  14. package/dist/channel/probe.js +149 -0
  15. package/dist/channel/resolve.d.ts +30 -0
  16. package/dist/channel/resolve.js +98 -0
  17. package/dist/channel/stream.d.ts +35 -0
  18. package/dist/channel/stream.js +127 -0
  19. package/dist/config/host-config-schema.d.ts +21 -0
  20. package/dist/config/host-config-schema.js +80 -0
  21. package/dist/config/loader.d.ts +7 -0
  22. package/dist/config/loader.js +38 -0
  23. package/dist/config/schema.d.ts +48 -0
  24. package/dist/config/schema.js +1 -0
  25. package/dist/errors/codes.d.ts +11 -0
  26. package/dist/errors/codes.js +10 -0
  27. package/dist/errors/exceptions.d.ts +7 -0
  28. package/dist/errors/exceptions.js +12 -0
  29. package/dist/inbound/mapper.d.ts +21 -0
  30. package/dist/inbound/mapper.js +22 -0
  31. package/dist/inbound/validator.d.ts +4 -0
  32. package/dist/inbound/validator.js +114 -0
  33. package/dist/index.d.ts +33 -0
  34. package/dist/index.js +31 -0
  35. package/dist/mapping/conversation-mapper.d.ts +1 -0
  36. package/dist/mapping/conversation-mapper.js +3 -0
  37. package/dist/mapping/sender-mapper.d.ts +1 -0
  38. package/dist/mapping/sender-mapper.js +3 -0
  39. package/dist/mapping/thread-mapper.d.ts +1 -0
  40. package/dist/mapping/thread-mapper.js +7 -0
  41. package/dist/openclaw-entry.d.ts +276 -0
  42. package/dist/openclaw-entry.js +728 -0
  43. package/dist/outbound/client.d.ts +6 -0
  44. package/dist/outbound/client.js +1 -0
  45. package/dist/outbound/controller.d.ts +15 -0
  46. package/dist/outbound/controller.js +28 -0
  47. package/dist/outbound/http-client.d.ts +23 -0
  48. package/dist/outbound/http-client.js +150 -0
  49. package/dist/outbound/mapper.d.ts +29 -0
  50. package/dist/outbound/mapper.js +19 -0
  51. package/dist/outbound/mock-client.d.ts +18 -0
  52. package/dist/outbound/mock-client.js +26 -0
  53. package/dist/outbound/sender.d.ts +3 -0
  54. package/dist/outbound/sender.js +5 -0
  55. package/dist/protocol/attachments.d.ts +10 -0
  56. package/dist/protocol/attachments.js +56 -0
  57. package/dist/protocol/dto.d.ts +46 -0
  58. package/dist/protocol/dto.js +1 -0
  59. package/dist/protocol/serializer.d.ts +1 -0
  60. package/dist/protocol/serializer.js +3 -0
  61. package/dist/security/nonce-store.d.ts +30 -0
  62. package/dist/security/nonce-store.js +32 -0
  63. package/dist/security/signer.d.ts +10 -0
  64. package/dist/security/signer.js +20 -0
  65. package/dist/security/verifier.d.ts +2 -0
  66. package/dist/security/verifier.js +20 -0
  67. package/dist/setup-entry.d.ts +351 -0
  68. package/dist/setup-entry.js +73 -0
  69. package/dist/utils/json.d.ts +1 -0
  70. package/dist/utils/json.js +3 -0
  71. package/dist/utils/log.d.ts +1 -0
  72. package/dist/utils/log.js +3 -0
  73. package/dist/utils/time.d.ts +1 -0
  74. package/dist/utils/time.js +3 -0
  75. package/openclaw.config.schema.json +80 -0
  76. package/openclaw.plugin.json +175 -0
  77. 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
+ };