@eclaw/openclaw-channel 1.0.9 → 1.0.11

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/dist/gateway.d.ts CHANGED
@@ -2,7 +2,8 @@
2
2
  * Gateway lifecycle: start an E-Claw account.
3
3
  *
4
4
  * 1. Resolve credentials from ctx.account or disk
5
- * 2. Start a local HTTP server to receive webhook callbacks
5
+ * 2. Register a per-session handler in the webhook-registry (served by the
6
+ * main OpenClaw gateway HTTP server at /eclaw-webhook — no separate port)
6
7
  * 3. Register callback URL with E-Claw backend
7
8
  * 4. Auto-bind entity if not already bound
8
9
  * 5. Keep the promise alive until abort signal fires
package/dist/gateway.js CHANGED
@@ -1,4 +1,3 @@
1
- import { createServer } from 'node:http';
2
1
  import { randomBytes } from 'node:crypto';
3
2
  import { readFileSync } from 'node:fs';
4
3
  import { join } from 'node:path';
@@ -7,12 +6,14 @@ import { resolveAccount } from './config.js';
7
6
  import { EClawClient } from './client.js';
8
7
  import { setClient } from './outbound.js';
9
8
  import { createWebhookHandler } from './webhook-handler.js';
9
+ import { registerWebhookToken, unregisterWebhookToken } from './webhook-registry.js';
10
10
  /**
11
11
  * Resolve account from ctx.
12
12
  *
13
13
  * OpenClaw may pass a pre-resolved account object in ctx.account,
14
14
  * or an empty config. Fall back to reading openclaw.json from disk.
15
15
  */
16
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
16
17
  function resolveAccountFromCtx(ctx) {
17
18
  // Preferred: OpenClaw passes the resolved account in ctx.account
18
19
  if (ctx.account?.apiKey) {
@@ -40,7 +41,8 @@ function resolveAccountFromCtx(ctx) {
40
41
  * Gateway lifecycle: start an E-Claw account.
41
42
  *
42
43
  * 1. Resolve credentials from ctx.account or disk
43
- * 2. Start a local HTTP server to receive webhook callbacks
44
+ * 2. Register a per-session handler in the webhook-registry (served by the
45
+ * main OpenClaw gateway HTTP server at /eclaw-webhook — no separate port)
44
46
  * 3. Register callback URL with E-Claw backend
45
47
  * 4. Auto-bind entity if not already bound
46
48
  * 5. Keep the promise alive until abort signal fires
@@ -59,7 +61,6 @@ export async function startAccount(ctx) {
59
61
  // Generate per-session callback token
60
62
  const callbackToken = randomBytes(32).toString('hex');
61
63
  // Webhook URL: account config > env var > warn
62
- const webhookPort = parseInt(process.env.ECLAW_WEBHOOK_PORT || '0') || 0;
63
64
  const publicUrl = account.webhookUrl?.replace(/\/$/, '')
64
65
  || process.env.ECLAW_WEBHOOK_URL?.replace(/\/$/, '');
65
66
  if (!publicUrl) {
@@ -68,72 +69,50 @@ export async function startAccount(ctx) {
68
69
  'or set ECLAW_WEBHOOK_URL env var. ' +
69
70
  'Example: https://your-openclaw-domain.com');
70
71
  }
71
- // Create webhook handler
72
- const handler = createWebhookHandler(callbackToken, accountId);
73
- // Parse JSON body for incoming requests
74
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
75
- const requestHandler = (req, res) => {
76
- if (req.method === 'POST' && req.url?.startsWith('/eclaw-webhook')) {
77
- let body = '';
78
- req.on('data', (chunk) => { body += chunk.toString(); });
79
- req.on('end', () => {
80
- try {
81
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
82
- req.body = JSON.parse(body);
83
- }
84
- catch {
85
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
86
- req.body = {};
87
- }
88
- handler(req, res);
89
- });
72
+ // The callback URL points to /eclaw-webhook on the main gateway HTTP server
73
+ const callbackUrl = `${publicUrl || 'http://localhost'}/eclaw-webhook`;
74
+ // Register handler in the per-token registry
75
+ // Pass ctx.cfg so the handler can dispatch to the correct OpenClaw agent
76
+ const handler = createWebhookHandler(callbackToken, accountId, ctx.cfg);
77
+ registerWebhookToken(callbackToken, accountId, handler);
78
+ console.log(`[E-Claw] Webhook registered at: ${callbackUrl}`);
79
+ try {
80
+ // Register callback with E-Claw backend
81
+ const regData = await client.registerCallback(callbackUrl, callbackToken);
82
+ console.log(`[E-Claw] Registered with E-Claw. Device: ${regData.deviceId}, Entities: ${regData.entities.length}`);
83
+ // Auto-bind entity if not already bound
84
+ const entity = regData.entities.find(e => e.entityId === account.entityId);
85
+ if (!entity?.isBound) {
86
+ console.log(`[E-Claw] Entity ${account.entityId} not bound, binding...`);
87
+ const bindData = await client.bindEntity(account.entityId, account.botName);
88
+ console.log(`[E-Claw] Bound entity ${account.entityId}, publicCode: ${bindData.publicCode}`);
90
89
  }
91
90
  else {
92
- res.writeHead(404);
93
- res.end('Not Found');
91
+ console.log(`[E-Claw] Entity ${account.entityId} already bound`);
92
+ const bindData = await client.bindEntity(account.entityId, account.botName);
93
+ console.log(`[E-Claw] Retrieved credentials for entity ${account.entityId}`);
94
+ void bindData;
94
95
  }
95
- };
96
- const server = createServer(requestHandler);
96
+ console.log(`[E-Claw] Account ${accountId} ready!`);
97
+ }
98
+ catch (err) {
99
+ console.error(`[E-Claw] Setup failed for account ${accountId}:`, err);
100
+ unregisterWebhookToken(callbackToken);
101
+ return;
102
+ }
103
+ // Keep the promise alive until abort signal fires
97
104
  return new Promise((resolve) => {
98
- server.listen(webhookPort, async () => {
99
- const addr = server.address();
100
- const actualPort = typeof addr === 'object' && addr ? addr.port : webhookPort;
101
- const baseUrl = publicUrl || `http://localhost:${actualPort}`;
102
- const callbackUrl = `${baseUrl}/eclaw-webhook`;
103
- console.log(`[E-Claw] Webhook server listening on port ${actualPort}`);
104
- console.log(`[E-Claw] Callback URL: ${callbackUrl}`);
105
- try {
106
- // Register callback with E-Claw backend
107
- const regData = await client.registerCallback(callbackUrl, callbackToken);
108
- console.log(`[E-Claw] Registered with E-Claw. Device: ${regData.deviceId}, Entities: ${regData.entities.length}`);
109
- // Auto-bind entity if not already bound
110
- const entity = regData.entities.find(e => e.entityId === account.entityId);
111
- if (!entity?.isBound) {
112
- console.log(`[E-Claw] Entity ${account.entityId} not bound, binding...`);
113
- const bindData = await client.bindEntity(account.entityId, account.botName);
114
- console.log(`[E-Claw] Bound entity ${account.entityId}, publicCode: ${bindData.publicCode}`);
115
- }
116
- else {
117
- console.log(`[E-Claw] Entity ${account.entityId} already bound`);
118
- const bindData = await client.bindEntity(account.entityId, account.botName);
119
- console.log(`[E-Claw] Retrieved credentials for entity ${account.entityId}`);
120
- void bindData;
121
- }
122
- console.log(`[E-Claw] Account ${accountId} ready!`);
123
- }
124
- catch (err) {
125
- console.error(`[E-Claw] Setup failed for account ${accountId}:`, err);
126
- }
127
- });
128
- // Cleanup on abort
129
105
  const signal = ctx.abortSignal;
130
106
  if (signal) {
131
107
  signal.addEventListener('abort', () => {
132
108
  console.log(`[E-Claw] Shutting down account ${accountId}`);
133
109
  client.unregisterCallback().catch(() => { });
134
- server.close();
110
+ unregisterWebhookToken(callbackToken);
135
111
  resolve();
136
112
  });
137
113
  }
114
+ else {
115
+ resolve();
116
+ }
138
117
  });
139
118
  }
package/dist/index.d.ts CHANGED
@@ -1,24 +1,3 @@
1
- /**
2
- * E-Claw Channel Plugin for OpenClaw.
3
- *
4
- * Installation:
5
- * npm install @eclaw/openclaw-channel
6
- *
7
- * Configuration (config.yaml):
8
- * channels:
9
- * eclaw:
10
- * accounts:
11
- * default:
12
- * apiKey: "eck_..."
13
- * apiSecret: "ecs_..."
14
- * apiBase: "https://eclawbot.com"
15
- * entityId: 0
16
- * botName: "My Bot"
17
- *
18
- * Environment variables:
19
- * ECLAW_WEBHOOK_URL - Public URL for receiving callbacks (required for production)
20
- * ECLAW_WEBHOOK_PORT - Port for webhook server (default: random)
21
- */
22
1
  declare const plugin: {
23
2
  id: string;
24
3
  name: string;
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { setPluginRuntime } from './runtime.js';
2
2
  import { eclawChannel } from './channel.js';
3
+ import { dispatchWebhook } from './webhook-registry.js';
3
4
  /**
4
5
  * E-Claw Channel Plugin for OpenClaw.
5
6
  *
@@ -12,15 +13,32 @@ import { eclawChannel } from './channel.js';
12
13
  * accounts:
13
14
  * default:
14
15
  * apiKey: "eck_..."
15
- * apiSecret: "ecs_..."
16
16
  * apiBase: "https://eclawbot.com"
17
17
  * entityId: 0
18
18
  * botName: "My Bot"
19
+ * webhookUrl: "https://your-openclaw-domain.com"
19
20
  *
20
- * Environment variables:
21
- * ECLAW_WEBHOOK_URL - Public URL for receiving callbacks (required for production)
22
- * ECLAW_WEBHOOK_PORT - Port for webhook server (default: random)
21
+ * The plugin registers /eclaw-webhook on the main OpenClaw gateway HTTP server,
22
+ * so no separate port is needed. Set webhookUrl to your OpenClaw public URL
23
+ * (e.g. https://eclaw2.zeabur.app) so E-Claw knows where to push messages.
23
24
  */
25
+ /** Parse JSON body from a raw incoming request */
26
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
27
+ function parseBody(req) {
28
+ return new Promise((resolve) => {
29
+ let body = '';
30
+ req.on('data', (chunk) => { body += chunk.toString(); });
31
+ req.on('end', () => {
32
+ try {
33
+ req.body = JSON.parse(body);
34
+ }
35
+ catch {
36
+ req.body = {};
37
+ }
38
+ resolve();
39
+ });
40
+ });
41
+ }
24
42
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
25
43
  const plugin = {
26
44
  id: 'eclaw',
@@ -30,6 +48,17 @@ const plugin = {
30
48
  register(api) {
31
49
  console.log('[E-Claw] Plugin loaded');
32
50
  setPluginRuntime(api.runtime);
51
+ // Register /eclaw-webhook on the main OpenClaw gateway HTTP server.
52
+ // Token-based routing is handled in dispatchWebhook() — each account
53
+ // registers its own handler keyed by a random per-session Bearer token.
54
+ api.registerHttpRoute({
55
+ path: '/eclaw-webhook',
56
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
57
+ handler: async (req, res) => {
58
+ await parseBody(req);
59
+ await dispatchWebhook(req, res);
60
+ },
61
+ });
33
62
  api.registerChannel({ plugin: eclawChannel });
34
63
  },
35
64
  };
@@ -2,6 +2,9 @@
2
2
  * Create an HTTP request handler for inbound messages from E-Claw.
3
3
  *
4
4
  * When a user sends a message on E-Claw, the backend POSTs structured JSON
5
- * to this webhook. We normalize it and dispatch to the OpenClaw agent.
5
+ * to this webhook. We normalize it into OpenClaw's native PascalCase context
6
+ * format and dispatch to the agent via dispatchReplyWithBufferedBlockDispatcher.
7
+ *
8
+ * The `deliver` callback sends the AI reply back to E-Claw via the API client.
6
9
  */
7
- export declare function createWebhookHandler(expectedToken: string, accountId: string): (req: any, res: any) => Promise<void>;
10
+ export declare function createWebhookHandler(expectedToken: string, accountId: string, cfg: any): (req: any, res: any) => Promise<void>;
@@ -1,11 +1,18 @@
1
1
  import { getPluginRuntime } from './runtime.js';
2
+ import { getClient } from './outbound.js';
2
3
  /**
3
4
  * Create an HTTP request handler for inbound messages from E-Claw.
4
5
  *
5
6
  * When a user sends a message on E-Claw, the backend POSTs structured JSON
6
- * to this webhook. We normalize it and dispatch to the OpenClaw agent.
7
+ * to this webhook. We normalize it into OpenClaw's native PascalCase context
8
+ * format and dispatch to the agent via dispatchReplyWithBufferedBlockDispatcher.
9
+ *
10
+ * The `deliver` callback sends the AI reply back to E-Claw via the API client.
7
11
  */
8
- export function createWebhookHandler(expectedToken, accountId) {
12
+ export function createWebhookHandler(expectedToken, accountId,
13
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
14
+ cfg // full openclaw config (ctx.cfg from startAccount)
15
+ ) {
9
16
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
10
17
  return async (req, res) => {
11
18
  // Verify callback token
@@ -22,36 +29,62 @@ export function createWebhookHandler(expectedToken, accountId) {
22
29
  // Dispatch to OpenClaw agent
23
30
  try {
24
31
  const rt = getPluginRuntime();
32
+ const client = getClient(accountId);
25
33
  const conversationId = msg.conversationId || `${msg.deviceId}:${msg.entityId}`;
26
- // Map E-Claw media types to OpenClaw types
27
- let media;
28
- if (msg.mediaType && msg.mediaUrl) {
29
- const type = msg.mediaType === 'photo' ? 'image'
30
- : msg.mediaType === 'voice' ? 'audio'
31
- : msg.mediaType === 'video' ? 'video'
32
- : 'file';
33
- media = { type, url: msg.mediaUrl };
34
- }
34
+ // Map E-Claw media type to OpenClaw media type
35
+ const ocMediaType = msg.mediaType === 'photo' ? 'image'
36
+ : msg.mediaType === 'voice' ? 'audio'
37
+ : msg.mediaType === 'video' ? 'video'
38
+ : msg.mediaType ? 'file'
39
+ : undefined;
40
+ // Build context in OpenClaw's native PascalCase format
41
+ // (same convention as Telegram/LINE/WhatsApp channels)
42
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
35
43
  const inboundCtx = {
36
- channelId: 'eclaw',
37
- accountId,
38
- conversationId,
39
- senderId: msg.from,
40
- text: msg.text || '',
41
- ...(media ? { media } : {}),
42
- metadata: {
43
- deviceId: msg.deviceId,
44
- entityId: msg.entityId,
45
- event: msg.event,
46
- fromEntityId: msg.fromEntityId,
47
- fromCharacter: msg.fromCharacter,
48
- isBroadcast: msg.isBroadcast,
49
- timestamp: msg.timestamp,
50
- },
44
+ Surface: 'eclaw',
45
+ Provider: 'eclaw',
46
+ OriginatingChannel: 'eclaw',
47
+ AccountId: accountId,
48
+ From: msg.from,
49
+ To: conversationId,
50
+ OriginatingTo: msg.from,
51
+ SessionKey: conversationId,
52
+ Body: msg.text || '',
53
+ RawBody: msg.text || '',
54
+ CommandBody: msg.text || '',
55
+ ChatType: 'direct',
56
+ ...(ocMediaType && msg.mediaUrl ? {
57
+ MediaType: ocMediaType,
58
+ MediaUrl: msg.mediaUrl,
59
+ } : {}),
51
60
  };
52
- // OpenClaw inbound dispatch pipeline
53
- const ctx = await rt.channel.reply.finalizeInboundContext(inboundCtx);
54
- await rt.channel.reply.dispatchReplyWithBufferedBlockDispatcher(ctx);
61
+ const ctxPayload = rt.channel.reply.finalizeInboundContext(inboundCtx);
62
+ await rt.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
63
+ ctx: ctxPayload,
64
+ cfg,
65
+ dispatcherOptions: {
66
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
67
+ deliver: async (payload) => {
68
+ if (!client)
69
+ return;
70
+ const text = typeof payload.text === 'string' ? payload.text.trim() : '';
71
+ if (text) {
72
+ await client.sendMessage(text, 'IDLE');
73
+ }
74
+ else if (payload.mediaUrl) {
75
+ const rawType = typeof payload.mediaType === 'string' ? payload.mediaType : '';
76
+ const mediaType = rawType === 'image' ? 'photo'
77
+ : rawType === 'audio' ? 'voice'
78
+ : rawType === 'video' ? 'video'
79
+ : 'file';
80
+ await client.sendMessage('', 'IDLE', mediaType, payload.mediaUrl);
81
+ }
82
+ },
83
+ onError: (err) => {
84
+ console.error('[E-Claw] Reply delivery error:', err);
85
+ },
86
+ },
87
+ });
55
88
  }
56
89
  catch (err) {
57
90
  console.error('[E-Claw] Webhook dispatch error:', err);
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Per-session webhook token registry.
3
+ *
4
+ * Each account generates a random callbackToken when it starts.
5
+ * The token is sent to E-Claw as part of the callback URL registration,
6
+ * and E-Claw echoes it back as `Authorization: Bearer <token>` on every push.
7
+ *
8
+ * The main route handler (registered on the gateway HTTP server) looks up
9
+ * the correct per-account handler by matching the Bearer token.
10
+ */
11
+ type WebhookHandler = (req: any, res: any) => Promise<void>;
12
+ export declare function registerWebhookToken(callbackToken: string, accountId: string, handler: WebhookHandler): void;
13
+ export declare function unregisterWebhookToken(callbackToken: string): void;
14
+ /**
15
+ * Dispatch an incoming webhook request to the correct account handler.
16
+ * Verifies the Bearer token and routes to the matching handler.
17
+ */
18
+ export declare function dispatchWebhook(req: any, res: any): Promise<void>;
19
+ export {};
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Per-session webhook token registry.
3
+ *
4
+ * Each account generates a random callbackToken when it starts.
5
+ * The token is sent to E-Claw as part of the callback URL registration,
6
+ * and E-Claw echoes it back as `Authorization: Bearer <token>` on every push.
7
+ *
8
+ * The main route handler (registered on the gateway HTTP server) looks up
9
+ * the correct per-account handler by matching the Bearer token.
10
+ */
11
+ const registry = new Map();
12
+ export function registerWebhookToken(callbackToken, accountId, handler) {
13
+ registry.set(callbackToken, { accountId, handler });
14
+ }
15
+ export function unregisterWebhookToken(callbackToken) {
16
+ registry.delete(callbackToken);
17
+ }
18
+ /**
19
+ * Dispatch an incoming webhook request to the correct account handler.
20
+ * Verifies the Bearer token and routes to the matching handler.
21
+ */
22
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
23
+ export async function dispatchWebhook(req, res) {
24
+ const authHeader = req.headers?.authorization;
25
+ if (!authHeader?.startsWith('Bearer ')) {
26
+ res.writeHead(401, { 'Content-Type': 'application/json' });
27
+ res.end(JSON.stringify({ error: 'Unauthorized' }));
28
+ return;
29
+ }
30
+ const token = authHeader.slice(7);
31
+ const entry = registry.get(token);
32
+ if (!entry) {
33
+ // Unknown token — likely a stale push after a server restart
34
+ res.writeHead(404, { 'Content-Type': 'application/json' });
35
+ res.end(JSON.stringify({ error: 'Unknown token' }));
36
+ return;
37
+ }
38
+ await entry.handler(req, res);
39
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eclaw/openclaw-channel",
3
- "version": "1.0.9",
3
+ "version": "1.0.11",
4
4
  "description": "E-Claw channel plugin for OpenClaw — AI chat platform for live wallpaper entities",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",