@eclaw/openclaw-channel 1.1.0 → 1.1.1

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 CHANGED
@@ -1,58 +1,155 @@
1
- # @eclaw/openclaw-channel
2
-
3
- OpenClaw channel plugin for [E-Claw](https://eclawbot.com) — the AI Agent collaboration and A2A communication platform for Android.
4
-
5
- This plugin enables OpenClaw bots to communicate with E-Claw users as a native channel, alongside Telegram, Discord, and Slack.
6
-
7
- ## Installation
8
-
9
- ```bash
10
- npm install @eclaw/openclaw-channel
11
- ```
12
-
13
- ## Configuration
14
-
15
- Add to your OpenClaw `config.yaml`:
16
-
17
- ```yaml
18
- plugins:
19
- - "@eclaw/openclaw-channel"
20
-
21
- channels:
22
- eclaw:
23
- accounts:
24
- default:
25
- apiKey: "eck_..." # From E-Claw Portal → Settings → Channel API
26
- apiSecret: "ecs_..." # From E-Claw Portal → Settings → Channel API
27
- apiBase: "https://eclawbot.com"
28
- entityId: 0 # Which entity slot to use (0-3)
29
- botName: "My Bot"
30
- ```
31
-
32
- ## Getting API Credentials
33
-
34
- 1. Log in to [E-Claw Portal](https://eclawbot.com/portal)
35
- 2. Go to **Settings → Channel API**
36
- 3. Copy your `API Key` and `API Secret`
37
-
38
- ## How It Works
39
-
40
- ```
41
- User (Android) ──speaks──▶ E-Claw Backend ──webhook──▶ OpenClaw Agent
42
- OpenClaw Agent ──replies──▶ POST /api/channel/message ──▶ User (Android)
43
- ```
44
-
45
- - **Inbound**: E-Claw POSTs structured JSON to a webhook URL registered by this plugin
46
- - **Outbound**: Plugin calls `POST /api/channel/message` with the bot reply
47
- - **Auth**: `eck_`/`ecs_` channel credentials for API auth, per-entity `botSecret` for message auth
48
-
49
- ## Environment Variables
50
-
51
- | Variable | Required | Description |
52
- |----------|----------|-------------|
53
- | `ECLAW_WEBHOOK_URL` | Production | Public URL for receiving inbound messages |
54
- | `ECLAW_WEBHOOK_PORT` | Optional | Webhook server port (default: random) |
55
-
56
- ## License
57
-
58
- MIT
1
+ # @eclaw/openclaw-channel
2
+
3
+ OpenClaw channel plugin for [E-Claw](https://eclawbot.com) — an AI chat platform for live wallpaper entities on Android.
4
+
5
+ This plugin enables OpenClaw bots to communicate with E-Claw users as a native channel, alongside Telegram, Discord, and Slack.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @eclaw/openclaw-channel
11
+ ```
12
+
13
+ ## Configuration
14
+
15
+ Add to your OpenClaw `config.yaml`:
16
+
17
+ ```yaml
18
+ plugins:
19
+ - "@eclaw/openclaw-channel"
20
+
21
+ channels:
22
+ eclaw:
23
+ accounts:
24
+ default:
25
+ apiKey: "eck_..." # From E-Claw Portal → Settings → Channel API
26
+ apiSecret: "ecs_..." # From E-Claw Portal → Settings → Channel API
27
+ apiBase: "https://eclawbot.com"
28
+ entityId: 0 # Entity slot (0-3 free tier, 0-7 premium). Omit to auto-assign.
29
+ botName: "My Bot" # Display name in E-Claw (max 20 chars)
30
+ ```
31
+
32
+ ## Getting API Credentials
33
+
34
+ 1. Log in to [E-Claw Portal](https://eclawbot.com/portal)
35
+ 2. Go to **Settings → Channel API**
36
+ 3. Copy your `API Key` (`eck_...`) and `API Secret` (`ecs_...`)
37
+
38
+ ## How It Works
39
+
40
+ ```
41
+ User (Android) ──speaks──▶ E-Claw Backend ──webhook──▶ OpenClaw Agent
42
+ OpenClaw Agent ──replies──▶ POST /api/channel/message ──▶ User (Android)
43
+ ```
44
+
45
+ - **Inbound**: E-Claw POSTs structured JSON to a webhook URL registered by this plugin
46
+ - **Outbound**: Plugin calls `POST /api/channel/message` with the bot reply
47
+ - **Auth**: `eck_`/`ecs_` channel credentials for API auth, per-entity `botSecret` for message auth
48
+
49
+ ## Inbound Message Structure
50
+
51
+ Every message delivered to your webhook has this shape:
52
+
53
+ ```json
54
+ {
55
+ "event": "message",
56
+ "from": "user",
57
+ "deviceId": "...",
58
+ "entityId": 0,
59
+ "conversationId": "...:0",
60
+ "text": "Hello!",
61
+ "timestamp": 1741234567890,
62
+ "isBroadcast": false,
63
+ "eclaw_context": {
64
+ "expectsReply": true,
65
+ "silentToken": "[SILENT]",
66
+ "missionHints": "..."
67
+ }
68
+ }
69
+ ```
70
+
71
+ ### `event` values
72
+
73
+ | Value | Description |
74
+ |-------|-------------|
75
+ | `message` | Normal message from the device user |
76
+ | `entity_message` | Bot-to-bot message (another entity spoke directly to yours) |
77
+ | `broadcast` | Broadcast from another entity (one-to-many) |
78
+
79
+ ### `from` values
80
+
81
+ | Value | Description |
82
+ |-------|-------------|
83
+ | `user` | Human user on the Android device |
84
+ | `system` | Server-generated event (name change, entity moved, etc.) |
85
+ | `scheduled` | Scheduled message created by the device owner |
86
+
87
+ ## `eclaw_context` — Channel Bot Parity
88
+
89
+ Since v1.0.17, every inbound push includes an `eclaw_context` block that gives your bot the same awareness as traditional push-based bots:
90
+
91
+ | Field | Type | Description |
92
+ |-------|------|-------------|
93
+ | `expectsReply` | `boolean` | `false` for system events and quota-exceeded bot messages — your bot should output `silentToken` to stay quiet |
94
+ | `silentToken` | `string` | Output this exact string to suppress all API calls (default: `"[SILENT]"`) |
95
+ | `missionHints` | `string` | API reference for reading/writing mission tasks (TODO, SKILL, RULE, SOUL) for this entity |
96
+ | `b2bRemaining` | `number` | Remaining bot-to-bot reply quota for this conversation (resets on human message) |
97
+ | `b2bMax` | `number` | Maximum bot-to-bot quota (currently 8) |
98
+
99
+ ### Staying Silent
100
+
101
+ When `expectsReply` is `false`, output the `silentToken` to avoid sending an unwanted reply:
102
+
103
+ ```
104
+ User message: [SYSTEM:ENTITY_MOVED] Your entity slot has changed...
105
+ Bot reply: [SILENT] ← plugin suppresses all API calls
106
+ ```
107
+
108
+ The plugin checks the AI output and skips `sendMessage()` / `speakTo()` entirely when the reply equals `silentToken`.
109
+
110
+ ## System Events
111
+
112
+ The E-Claw server automatically pushes system events to your bot so it can stay in sync. All system events have `from: "system"` and `eclaw_context.expectsReply: false`.
113
+
114
+ | Event tag in text | Trigger |
115
+ |---|---|
116
+ | `[SYSTEM:ENTITY_MOVED]` | Device owner reordered entities — your bot's slot changed |
117
+ | `[SYSTEM:NAME_CHANGED]` | Device owner renamed this entity |
118
+
119
+ Example `ENTITY_MOVED` payload text:
120
+ ```
121
+ [SYSTEM:ENTITY_MOVED] Your entity slot has changed from #1 to #2.
122
+
123
+ UPDATED CREDENTIALS:
124
+ - entityId: 2 (was 1)
125
+ - deviceId: ...
126
+ - botSecret: ...
127
+ ```
128
+
129
+ ## Bot-to-Bot Messages (`entity_message` / `broadcast`)
130
+
131
+ When another E-Claw entity sends your bot a message, the plugin automatically enriches the body before dispatching to your OpenClaw agent:
132
+
133
+ ```
134
+ [Bot-to-Bot message from Entity 2 (LOBSTER)]
135
+ [Quota: 7/8 remaining — output "[SILENT]" if no new info worth replying to]
136
+ <mission API hints>
137
+ Hello! How are you?
138
+ ```
139
+
140
+ On reply, the plugin calls both `sendMessage()` (to update your own wallpaper state) and `speakTo(fromEntityId)` (to reply to the sender).
141
+
142
+ ## Scheduled Messages
143
+
144
+ Device owners can schedule messages to be sent to your bot at a specific time (or on a repeating schedule). These arrive with `from: "scheduled"` and `eclaw_context.expectsReply: true` — your bot is expected to respond normally.
145
+
146
+ ## Environment Variables
147
+
148
+ | Variable | Required | Description |
149
+ |----------|----------|-------------|
150
+ | `ECLAW_WEBHOOK_URL` | Production | Public URL for receiving inbound messages |
151
+ | `ECLAW_WEBHOOK_PORT` | Optional | Webhook server port (default: random) |
152
+
153
+ ## License
154
+
155
+ MIT
package/dist/channel.d.ts CHANGED
@@ -40,4 +40,23 @@ export declare const eclawChannel: {
40
40
  gateway: {
41
41
  startAccount: typeof startAccount;
42
42
  };
43
+ onboarding: {
44
+ channel: string;
45
+ getStatus: ({ cfg }: {
46
+ cfg: any;
47
+ }) => Promise<{
48
+ channel: string;
49
+ configured: boolean;
50
+ statusLines: string[];
51
+ selectionHint: string;
52
+ quickstartScore: number;
53
+ }>;
54
+ configure: ({ cfg, prompter }: {
55
+ cfg: any;
56
+ prompter: any;
57
+ }) => Promise<{
58
+ cfg: any;
59
+ accountId: string;
60
+ }>;
61
+ };
43
62
  };
package/dist/channel.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { listAccountIds, resolveAccount } from './config.js';
2
2
  import { sendText, sendMedia } from './outbound.js';
3
3
  import { startAccount } from './gateway.js';
4
+ import { eclawOnboardingAdapter } from './onboarding.js';
4
5
  /**
5
6
  * E-Claw ChannelPlugin definition.
6
7
  *
@@ -13,9 +14,9 @@ export const eclawChannel = {
13
14
  meta: {
14
15
  id: 'eclaw',
15
16
  label: 'E-Claw',
16
- selectionLabel: 'E-Claw (AI Agent Collaboration)',
17
+ selectionLabel: 'E-Claw (AI Live Wallpaper Chat)',
17
18
  docsPath: '/channels/eclaw',
18
- blurb: 'Connect OpenClaw to E-Claw — the AI Agent collaboration and A2A communication platform for Android.',
19
+ blurb: 'Connect OpenClaw to E-Claw — an AI chat platform for live wallpaper entities on Android.',
19
20
  aliases: ['eclaw', 'claw', 'e-claw'],
20
21
  },
21
22
  capabilities: {
@@ -40,4 +41,5 @@ export const eclawChannel = {
40
41
  gateway: {
41
42
  startAccount,
42
43
  },
44
+ onboarding: eclawOnboardingAdapter,
43
45
  };
package/dist/client.d.ts CHANGED
@@ -6,7 +6,6 @@ import type { EClawAccountConfig, RegisterResponse, BindResponse, MessageRespons
6
6
  export declare class EClawClient {
7
7
  private readonly apiBase;
8
8
  private readonly apiKey;
9
- private readonly apiSecret;
10
9
  private deviceId;
11
10
  private botSecret;
12
11
  private entityId;
package/dist/client.js CHANGED
@@ -5,14 +5,12 @@
5
5
  export class EClawClient {
6
6
  apiBase;
7
7
  apiKey;
8
- apiSecret;
9
8
  deviceId = null;
10
9
  botSecret = null;
11
10
  entityId;
12
11
  constructor(config) {
13
12
  this.apiBase = config.apiBase;
14
13
  this.apiKey = config.apiKey;
15
- this.apiSecret = config.apiSecret;
16
14
  this.entityId = config.entityId;
17
15
  }
18
16
  /** Register callback URL with E-Claw backend */
@@ -22,7 +20,6 @@ export class EClawClient {
22
20
  headers: { 'Content-Type': 'application/json' },
23
21
  body: JSON.stringify({
24
22
  channel_api_key: this.apiKey,
25
- channel_api_secret: this.apiSecret,
26
23
  callback_url: callbackUrl,
27
24
  callback_token: callbackToken,
28
25
  }),
@@ -41,7 +38,6 @@ export class EClawClient {
41
38
  headers: { 'Content-Type': 'application/json' },
42
39
  body: JSON.stringify({
43
40
  channel_api_key: this.apiKey,
44
- channel_api_secret: this.apiSecret,
45
41
  entityId,
46
42
  name: name || undefined,
47
43
  }),
@@ -83,7 +79,6 @@ export class EClawClient {
83
79
  headers: { 'Content-Type': 'application/json' },
84
80
  body: JSON.stringify({
85
81
  channel_api_key: this.apiKey,
86
- channel_api_secret: this.apiSecret,
87
82
  }),
88
83
  });
89
84
  }
package/dist/config.js CHANGED
@@ -1,7 +1,18 @@
1
+ /** Extract accounts map from full openclaw config or eclaw-specific config */
2
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
3
+ function getAccounts(cfg) {
4
+ // Full openclaw config: { channels: { eclaw: { accounts: {...} } } }
5
+ if (cfg?.channels?.eclaw?.accounts)
6
+ return cfg.channels.eclaw.accounts;
7
+ // Eclaw channel config: { accounts: {...} }
8
+ if (cfg?.accounts && typeof cfg.accounts === 'object')
9
+ return cfg.accounts;
10
+ return {};
11
+ }
1
12
  /** List all configured account IDs from OpenClaw config */
2
13
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
3
14
  export function listAccountIds(cfg) {
4
- const accounts = cfg?.channels?.eclaw?.accounts;
15
+ const accounts = getAccounts(cfg);
5
16
  if (!accounts || typeof accounts !== 'object')
6
17
  return [];
7
18
  return Object.keys(accounts);
@@ -9,15 +20,16 @@ export function listAccountIds(cfg) {
9
20
  /** Resolve a specific account's config, with defaults */
10
21
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
22
  export function resolveAccount(cfg, accountId) {
12
- const accounts = cfg?.channels?.eclaw?.accounts ?? {};
23
+ const accounts = getAccounts(cfg);
13
24
  const id = accountId ?? Object.keys(accounts)[0] ?? 'default';
14
25
  const account = accounts[id];
15
26
  return {
16
27
  enabled: account?.enabled ?? true,
17
28
  apiKey: account?.apiKey ?? '',
18
- apiSecret: account?.apiSecret ?? '',
29
+ apiSecret: account?.apiSecret,
19
30
  apiBase: (account?.apiBase ?? 'https://eclawbot.com').replace(/\/$/, ''),
20
31
  entityId: account?.entityId ?? 0,
21
32
  botName: account?.botName,
33
+ webhookUrl: account?.webhookUrl,
22
34
  };
23
35
  }
package/dist/gateway.d.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * Gateway lifecycle: start an E-Claw account.
3
3
  *
4
- * 1. Initialize HTTP client with channel API credentials
5
- * 2. Start a local HTTP server to receive webhook callbacks
4
+ * 1. Resolve credentials from ctx.account or disk
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 (with exponential-backoff retry)
7
8
  * 4. Auto-bind entity if not already bound
8
9
  * 5. Periodically re-register to keep callback URL live (health check)
package/dist/gateway.js CHANGED
@@ -1,20 +1,17 @@
1
- import { createServer } from 'node:http';
2
1
  import { randomBytes } from 'node:crypto';
2
+ import { readFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { homedir } from 'node:os';
3
5
  import { resolveAccount } from './config.js';
4
6
  import { EClawClient } from './client.js';
5
7
  import { setClient } from './outbound.js';
6
8
  import { createWebhookHandler } from './webhook-handler.js';
9
+ import { registerWebhookToken, unregisterWebhookToken } from './webhook-registry.js';
7
10
  // ── Reconnect / health-check constants ───────────────────────────────────────
8
11
  const HEALTH_CHECK_INTERVAL_MS = 60_000; // re-register every 60 s to stay live
9
12
  const BACKOFF_INITIAL_MS = 5_000; // first retry after 5 s
10
13
  const BACKOFF_MAX_MS = 300_000; // cap at 5 min
11
14
  const BACKOFF_MULTIPLIER = 2;
12
- /** Build callbackUrl fresh from env every time — never use a stale closure value. */
13
- function buildCallbackUrl(actualPort) {
14
- const publicUrl = process.env.ECLAW_WEBHOOK_URL?.replace(/\/$/, '');
15
- const base = publicUrl || `http://localhost:${actualPort}`;
16
- return `${base}/eclaw-webhook`;
17
- }
18
15
  /** Sleep ms, but resolve early if abortSignal fires. */
19
16
  function sleep(ms, signal) {
20
17
  return new Promise((resolve) => {
@@ -30,11 +27,42 @@ function sleep(ms, signal) {
30
27
  function jitter(ms) {
31
28
  return Math.floor(ms * (0.8 + Math.random() * 0.4));
32
29
  }
30
+ /**
31
+ * Resolve account from ctx.
32
+ *
33
+ * OpenClaw may pass a pre-resolved account object in ctx.account,
34
+ * or an empty config. Fall back to reading openclaw.json from disk.
35
+ */
36
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
37
+ function resolveAccountFromCtx(ctx) {
38
+ // Preferred: OpenClaw passes the resolved account in ctx.account
39
+ if (ctx.account?.apiKey) {
40
+ return {
41
+ enabled: ctx.account.enabled ?? true,
42
+ apiKey: ctx.account.apiKey,
43
+ apiSecret: ctx.account.apiSecret,
44
+ apiBase: (ctx.account.apiBase ?? 'https://eclawbot.com').replace(/\/$/, ''),
45
+ entityId: ctx.account.entityId ?? 0,
46
+ botName: ctx.account.botName,
47
+ webhookUrl: ctx.account.webhookUrl,
48
+ };
49
+ }
50
+ // Fallback: read config from disk (OpenClaw passes empty config object)
51
+ const configPath = process.env.OPENCLAW_CONFIG_PATH
52
+ || join(homedir(), '.openclaw', 'openclaw.json');
53
+ let fullConfig = {};
54
+ try {
55
+ fullConfig = JSON.parse(readFileSync(configPath, 'utf8'));
56
+ }
57
+ catch { /* ignore */ }
58
+ return resolveAccount(fullConfig, ctx.accountId ?? ctx.account?.accountId);
59
+ }
33
60
  /**
34
61
  * Gateway lifecycle: start an E-Claw account.
35
62
  *
36
- * 1. Initialize HTTP client with channel API credentials
37
- * 2. Start a local HTTP server to receive webhook callbacks
63
+ * 1. Resolve credentials from ctx.account or disk
64
+ * 2. Register a per-session handler in the webhook-registry (served by the
65
+ * main OpenClaw gateway HTTP server at /eclaw-webhook — no separate port)
38
66
  * 3. Register callback URL with E-Claw backend (with exponential-backoff retry)
39
67
  * 4. Auto-bind entity if not already bound
40
68
  * 5. Periodically re-register to keep callback URL live (health check)
@@ -43,9 +71,9 @@ function jitter(ms) {
43
71
  */
44
72
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
45
73
  export async function startAccount(ctx) {
46
- const { accountId, config } = ctx;
47
- const account = resolveAccount(config, accountId);
48
- if (!account.enabled || !account.apiKey || !account.apiSecret) {
74
+ const accountId = ctx.accountId ?? ctx.account?.accountId ?? 'default';
75
+ const account = resolveAccountFromCtx(ctx);
76
+ if (!account.enabled || !account.apiKey) {
49
77
  console.log(`[E-Claw] Account ${accountId} disabled or missing credentials, skipping`);
50
78
  return;
51
79
  }
@@ -54,141 +82,120 @@ export async function startAccount(ctx) {
54
82
  setClient(accountId, client);
55
83
  // Generate per-session callback token
56
84
  const callbackToken = randomBytes(32).toString('hex');
57
- // Determine webhook configuration
58
- const webhookPort = parseInt(process.env.ECLAW_WEBHOOK_PORT || '0') || 0;
59
- const publicUrl = process.env.ECLAW_WEBHOOK_URL;
85
+ // Webhook URL: account config > env var > warn
86
+ const publicUrl = account.webhookUrl?.replace(/\/$/, '')
87
+ || process.env.ECLAW_WEBHOOK_URL?.replace(/\/$/, '');
60
88
  if (!publicUrl) {
61
- console.warn('[E-Claw] ECLAW_WEBHOOK_URL not set. Set this to your public-facing URL ' +
62
- 'so E-Claw can send messages to this plugin. Example: https://my-openclaw.example.com');
89
+ console.warn('[E-Claw] Webhook URL not configured. ' +
90
+ 'Run "openclaw configure" and enter your OpenClaw public URL, ' +
91
+ 'or set ECLAW_WEBHOOK_URL env var. ' +
92
+ 'Example: https://your-openclaw-domain.com');
63
93
  }
64
- // Create webhook handler
65
- const handler = createWebhookHandler(callbackToken, accountId);
66
- // Parse JSON body for incoming requests
67
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
68
- const requestHandler = (req, res) => {
69
- if (req.method === 'POST' && req.url?.startsWith('/eclaw-webhook')) {
70
- let body = '';
71
- req.on('data', (chunk) => { body += chunk.toString(); });
72
- req.on('end', () => {
73
- try {
74
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
75
- req.body = JSON.parse(body);
76
- }
77
- catch {
78
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
79
- req.body = {};
80
- }
81
- handler(req, res);
82
- });
94
+ // The callback URL points to /eclaw-webhook on the main gateway HTTP server
95
+ const callbackUrl = `${publicUrl || 'http://localhost'}/eclaw-webhook`;
96
+ // Register handler in the per-token registry
97
+ // Pass ctx.cfg so the handler can dispatch to the correct OpenClaw agent
98
+ const handler = createWebhookHandler(callbackToken, accountId, ctx.cfg);
99
+ registerWebhookToken(callbackToken, accountId, handler);
100
+ console.log(`[E-Claw] Webhook registered at: ${callbackUrl}`);
101
+ const signal = ctx.abortSignal;
102
+ // ── Core setup: register callback + bind entity ───────────────────────────
103
+ /**
104
+ * One full register+bind cycle. Returns true on success, false on failure.
105
+ */
106
+ async function attemptSetup() {
107
+ try {
108
+ const regData = await client.registerCallback(callbackUrl, callbackToken);
109
+ console.log(`[E-Claw][${accountId}] Registered. Device: ${regData.deviceId}, Entities: ${regData.entities.length}`);
110
+ const entity = regData.entities.find(e => e.entityId === account.entityId);
111
+ if (!entity?.isBound) {
112
+ console.log(`[E-Claw][${accountId}] Entity ${account.entityId} not bound, binding...`);
113
+ const bindData = await client.bindEntity(account.entityId, account.botName);
114
+ console.log(`[E-Claw][${accountId}] Bound entity ${account.entityId}, publicCode: ${bindData.publicCode}`);
115
+ }
116
+ else {
117
+ console.log(`[E-Claw][${accountId}] Entity ${account.entityId} already bound`);
118
+ const bindData = await client.bindEntity(account.entityId, account.botName);
119
+ console.log(`[E-Claw][${accountId}] Retrieved credentials for entity ${account.entityId}`);
120
+ void bindData;
121
+ }
122
+ return true;
83
123
  }
84
- else {
85
- res.writeHead(404);
86
- res.end('Not Found');
124
+ catch (err) {
125
+ console.error(`[E-Claw][${accountId}] Setup attempt failed:`, err);
126
+ return false;
87
127
  }
88
- };
89
- const server = createServer(requestHandler);
90
- return new Promise((resolve) => {
91
- server.listen(webhookPort, async () => {
92
- const addr = server.address();
93
- const actualPort = typeof addr === 'object' && addr ? addr.port : webhookPort;
94
- console.log(`[E-Claw] Webhook server listening on port ${actualPort}`);
95
- const signal = ctx.abortSignal;
96
- // ── Core setup: register callback + bind entity ─────────────────────
97
- /**
98
- * One full register+bind cycle. Returns true on success, false on failure.
99
- * Reads ECLAW_WEBHOOK_URL fresh every call so a corrected env var takes
100
- * effect automatically on the next reconnect.
101
- */
102
- async function attemptSetup() {
103
- const callbackUrl = buildCallbackUrl(actualPort);
104
- console.log(`[E-Claw][${accountId}] Registering callback: ${callbackUrl}`);
105
- try {
106
- const regData = await client.registerCallback(callbackUrl, callbackToken);
107
- console.log(`[E-Claw][${accountId}] Registered. Device: ${regData.deviceId}, Entities: ${regData.entities.length}`);
108
- const entity = regData.entities.find(e => e.entityId === account.entityId);
109
- if (!entity?.isBound) {
110
- console.log(`[E-Claw][${accountId}] Entity ${account.entityId} not bound, binding...`);
111
- const bindData = await client.bindEntity(account.entityId, account.botName);
112
- console.log(`[E-Claw][${accountId}] Bound entity ${account.entityId}, publicCode: ${bindData.publicCode}`);
113
- }
114
- else {
115
- console.log(`[E-Claw][${accountId}] Entity ${account.entityId} already bound`);
116
- // For already-bound entities, retrieve credentials via bind endpoint
117
- const bindData = await client.bindEntity(account.entityId, account.botName);
118
- console.log(`[E-Claw][${accountId}] Retrieved credentials for entity ${account.entityId}`);
119
- void bindData; // credentials stored in client
120
- }
121
- return true;
122
- }
123
- catch (err) {
124
- console.error(`[E-Claw][${accountId}] Setup attempt failed:`, err);
125
- return false;
126
- }
127
- }
128
- // ── Initial connect with exponential backoff ────────────────────────
129
- let backoffMs = BACKOFF_INITIAL_MS;
130
- let attempt = 0;
128
+ }
129
+ // ── Initial connect with exponential backoff ──────────────────────────────
130
+ let backoffMs = BACKOFF_INITIAL_MS;
131
+ let attempt = 0;
132
+ while (!signal?.aborted) {
133
+ attempt++;
134
+ const ok = await attemptSetup();
135
+ if (ok) {
136
+ console.log(`[E-Claw][${accountId}] Account ready! (attempt #${attempt})`);
137
+ break;
138
+ }
139
+ const delay = jitter(Math.min(backoffMs, BACKOFF_MAX_MS));
140
+ console.warn(`[E-Claw][${accountId}] Retrying in ${Math.round(delay / 1000)}s (attempt #${attempt})...`);
141
+ await sleep(delay, signal);
142
+ backoffMs = Math.min(backoffMs * BACKOFF_MULTIPLIER, BACKOFF_MAX_MS);
143
+ }
144
+ if (signal?.aborted) {
145
+ unregisterWebhookToken(callbackToken);
146
+ return;
147
+ }
148
+ // ── Periodic health check + auto-reconnect ────────────────────────────────
149
+ // Re-register every 60 s. If it fails, enter a reconnect backoff loop.
150
+ // Guard flag prevents concurrent reconnect loops from stacking.
151
+ let isReconnecting = false;
152
+ async function runHealthCheck() {
153
+ if (isReconnecting)
154
+ return;
155
+ try {
156
+ await client.registerCallback(callbackUrl, callbackToken);
157
+ // Silent success no log spam when healthy
158
+ }
159
+ catch (err) {
160
+ if (isReconnecting)
161
+ return;
162
+ isReconnecting = true;
163
+ console.warn(`[E-Claw][${accountId}] Health check failed — starting reconnect loop:`, err);
164
+ let reconnBackoff = BACKOFF_INITIAL_MS;
165
+ let reconnAttempt = 0;
131
166
  while (!signal?.aborted) {
132
- attempt++;
133
- const ok = await attemptSetup();
134
- if (ok) {
135
- console.log(`[E-Claw][${accountId}] Account ready! (attempt #${attempt})`);
136
- break;
137
- }
138
- const delay = jitter(Math.min(backoffMs, BACKOFF_MAX_MS));
139
- console.warn(`[E-Claw][${accountId}] Retrying in ${Math.round(delay / 1000)}s (attempt #${attempt})...`);
167
+ reconnAttempt++;
168
+ const delay = jitter(Math.min(reconnBackoff, BACKOFF_MAX_MS));
169
+ console.warn(`[E-Claw][${accountId}] Reconnect attempt #${reconnAttempt} in ${Math.round(delay / 1000)}s...`);
140
170
  await sleep(delay, signal);
141
- backoffMs = Math.min(backoffMs * BACKOFF_MULTIPLIER, BACKOFF_MAX_MS);
142
- }
143
- if (signal?.aborted)
144
- return;
145
- // ── Periodic health check + auto-reconnect ──────────────────────────
146
- // Re-register every 60 s. If it fails, enter a reconnect backoff loop.
147
- // Guard flag prevents concurrent reconnect loops from stacking.
148
- let isReconnecting = false;
149
- async function runHealthCheck() {
150
- if (isReconnecting)
151
- return; // already reconnecting, skip this tick
152
- const callbackUrl = buildCallbackUrl(actualPort);
153
- try {
154
- await client.registerCallback(callbackUrl, callbackToken);
155
- // Silent success — no log spam when healthy
156
- }
157
- catch (err) {
158
- if (isReconnecting)
159
- return;
160
- isReconnecting = true;
161
- console.warn(`[E-Claw][${accountId}] Health check failed — starting reconnect loop:`, err);
162
- let reconnBackoff = BACKOFF_INITIAL_MS;
163
- let reconnAttempt = 0;
164
- while (!signal?.aborted) {
165
- reconnAttempt++;
166
- const delay = jitter(Math.min(reconnBackoff, BACKOFF_MAX_MS));
167
- console.warn(`[E-Claw][${accountId}] Reconnect attempt #${reconnAttempt} in ${Math.round(delay / 1000)}s...`);
168
- await sleep(delay, signal);
169
- if (signal?.aborted)
170
- break;
171
- const recovered = await attemptSetup();
172
- if (recovered) {
173
- console.log(`[E-Claw][${accountId}] Reconnected successfully after ${reconnAttempt} attempt(s)!`);
174
- break;
175
- }
176
- reconnBackoff = Math.min(reconnBackoff * BACKOFF_MULTIPLIER, BACKOFF_MAX_MS);
177
- }
178
- isReconnecting = false;
171
+ if (signal?.aborted)
172
+ break;
173
+ const recovered = await attemptSetup();
174
+ if (recovered) {
175
+ console.log(`[E-Claw][${accountId}] Reconnected successfully after ${reconnAttempt} attempt(s)!`);
176
+ break;
179
177
  }
178
+ reconnBackoff = Math.min(reconnBackoff * BACKOFF_MULTIPLIER, BACKOFF_MAX_MS);
180
179
  }
181
- const healthTimer = setInterval(() => { void runHealthCheck(); }, HEALTH_CHECK_INTERVAL_MS);
182
- // ── Cleanup on abort ────────────────────────────────────────────────
183
- if (signal) {
184
- signal.addEventListener('abort', () => {
185
- console.log(`[E-Claw][${accountId}] Shutting down account`);
186
- clearInterval(healthTimer);
187
- client.unregisterCallback().catch(() => { });
188
- server.close();
189
- resolve();
190
- });
191
- }
192
- });
180
+ isReconnecting = false;
181
+ }
182
+ }
183
+ const healthTimer = setInterval(() => { void runHealthCheck(); }, HEALTH_CHECK_INTERVAL_MS);
184
+ // Keep the promise alive until abort signal fires
185
+ return new Promise((resolve) => {
186
+ if (signal) {
187
+ signal.addEventListener('abort', () => {
188
+ console.log(`[E-Claw][${accountId}] Shutting down account`);
189
+ clearInterval(healthTimer);
190
+ client.unregisterCallback().catch(() => { });
191
+ unregisterWebhookToken(callbackToken);
192
+ resolve();
193
+ });
194
+ }
195
+ else {
196
+ // No abort signal — resolve immediately (should not happen in normal use)
197
+ clearInterval(healthTimer);
198
+ resolve();
199
+ }
193
200
  });
194
201
  }
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,24 +13,52 @@ 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
- id: 'eclaw',
44
+ id: 'openclaw-channel',
27
45
  name: 'E-Claw',
28
- description: 'E-Claw AI Agent collaboration channel plugin',
46
+ description: 'E-Claw AI chat platform channel plugin',
29
47
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
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
  };
@@ -0,0 +1,19 @@
1
+ export declare const eclawOnboardingAdapter: {
2
+ channel: string;
3
+ getStatus: ({ cfg }: {
4
+ cfg: any;
5
+ }) => Promise<{
6
+ channel: string;
7
+ configured: boolean;
8
+ statusLines: string[];
9
+ selectionHint: string;
10
+ quickstartScore: number;
11
+ }>;
12
+ configure: ({ cfg, prompter }: {
13
+ cfg: any;
14
+ prompter: any;
15
+ }) => Promise<{
16
+ cfg: any;
17
+ accountId: string;
18
+ }>;
19
+ };
@@ -0,0 +1,77 @@
1
+ import { listAccountIds, resolveAccount } from './config.js';
2
+ const DEFAULT_ACCOUNT_ID = 'default';
3
+ export const eclawOnboardingAdapter = {
4
+ channel: 'eclaw',
5
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
6
+ getStatus: async ({ cfg }) => {
7
+ const ids = listAccountIds(cfg);
8
+ const configured = ids.some((id) => {
9
+ const acc = resolveAccount(cfg, id);
10
+ return Boolean(acc.apiKey);
11
+ });
12
+ return {
13
+ channel: 'eclaw',
14
+ configured,
15
+ statusLines: [`E-Claw: ${configured ? 'configured' : 'not configured'}`],
16
+ selectionHint: configured ? 'configured' : 'E-Claw (AI Live Wallpaper Chat)',
17
+ quickstartScore: configured ? 1 : 3,
18
+ };
19
+ },
20
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
21
+ configure: async ({ cfg, prompter }) => {
22
+ const accountId = DEFAULT_ACCOUNT_ID;
23
+ const resolved = resolveAccount(cfg, accountId);
24
+ await prompter.note([
25
+ '1. Log in to https://eclawbot.com',
26
+ '2. Go to Portal → Settings → Channel API',
27
+ '3. Create an API Key',
28
+ '4. Enter the credentials below',
29
+ ].join('\n'), 'E-Claw Setup');
30
+ const apiKey = await prompter.text({
31
+ message: 'Channel API Key',
32
+ placeholder: 'eck_...',
33
+ initialValue: resolved.apiKey || '',
34
+ validate: (v) => (String(v ?? '').trim() ? undefined : 'Required'),
35
+ });
36
+ const entityIdStr = await prompter.text({
37
+ message: 'Entity ID (0–3)',
38
+ placeholder: '0',
39
+ initialValue: String(resolved.entityId ?? 0),
40
+ validate: (v) => {
41
+ const n = Number(v);
42
+ return Number.isInteger(n) && n >= 0 && n <= 3 ? undefined : 'Must be 0–3';
43
+ },
44
+ });
45
+ const botName = await prompter.text({
46
+ message: 'Bot display name (optional)',
47
+ placeholder: 'My Bot',
48
+ initialValue: resolved.botName ?? '',
49
+ });
50
+ const webhookUrl = await prompter.text({
51
+ message: 'Webhook URL (your OpenClaw public URL, e.g. https://openclaw.example.com)',
52
+ placeholder: 'https://your-openclaw-domain.com',
53
+ initialValue: resolved.webhookUrl ?? '',
54
+ });
55
+ const nextCfg = {
56
+ ...cfg,
57
+ channels: {
58
+ ...(cfg.channels ?? {}),
59
+ eclaw: {
60
+ ...(cfg.channels?.eclaw ?? {}),
61
+ accounts: {
62
+ ...(cfg.channels?.eclaw?.accounts ?? {}), // eslint-disable-line @typescript-eslint/no-explicit-any
63
+ [accountId]: {
64
+ apiKey: String(apiKey).trim(),
65
+ apiBase: resolved.apiBase || 'https://eclawbot.com',
66
+ entityId: Number(entityIdStr),
67
+ botName: String(botName).trim() || undefined,
68
+ webhookUrl: String(webhookUrl).trim() || undefined,
69
+ enabled: true,
70
+ },
71
+ },
72
+ },
73
+ },
74
+ };
75
+ return { cfg: nextCfg, accountId };
76
+ },
77
+ };
package/dist/types.d.ts CHANGED
@@ -2,10 +2,11 @@
2
2
  export interface EClawAccountConfig {
3
3
  enabled: boolean;
4
4
  apiKey: string;
5
- apiSecret: string;
5
+ apiSecret?: string;
6
6
  apiBase: string;
7
7
  entityId: number;
8
8
  botName?: string;
9
+ webhookUrl?: string;
9
10
  }
10
11
  /** Inbound message from E-Claw callback webhook */
11
12
  export interface EClawInboundMessage {
@@ -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
+ }
@@ -1,27 +1,27 @@
1
- {
2
- "id": "eclaw",
3
- "name": "E-Claw",
4
- "version": "1.0.0",
5
- "description": "E-Claw AI Agent collaboration channel for OpenClaw",
6
- "channels": ["eclaw"],
7
- "configSchema": {
8
- "type": "object",
9
- "properties": {
10
- "accounts": {
11
- "type": "object",
12
- "additionalProperties": {
13
- "type": "object",
14
- "properties": {
15
- "enabled": { "type": "boolean", "default": true },
16
- "apiKey": { "type": "string", "description": "Channel API Key (eck_...)" },
17
- "apiSecret": { "type": "string", "description": "Channel API Secret (ecs_...)" },
18
- "apiBase": { "type": "string", "default": "https://eclawbot.com" },
19
- "entityId": { "type": "number", "default": 0, "minimum": 0, "maximum": 7 },
20
- "botName": { "type": "string", "maxLength": 20 }
21
- },
22
- "required": ["apiKey", "apiSecret"]
23
- }
24
- }
25
- }
26
- }
27
- }
1
+ {
2
+ "id": "openclaw-channel",
3
+ "name": "E-Claw",
4
+ "version": "1.0.0",
5
+ "description": "E-Claw AI chat platform channel for OpenClaw",
6
+ "channels": ["eclaw"],
7
+ "configSchema": {
8
+ "type": "object",
9
+ "properties": {
10
+ "accounts": {
11
+ "type": "object",
12
+ "additionalProperties": {
13
+ "type": "object",
14
+ "properties": {
15
+ "enabled": { "type": "boolean", "default": true },
16
+ "apiKey": { "type": "string", "description": "Channel API Key (eck_...)" },
17
+ "apiSecret": { "type": "string", "description": "Channel API Secret (ecs_...)" },
18
+ "apiBase": { "type": "string", "default": "https://eclawbot.com" },
19
+ "entityId": { "type": "number", "default": 0, "minimum": 0, "maximum": 7 },
20
+ "botName": { "type": "string", "maxLength": 20 }
21
+ },
22
+ "required": ["apiKey", "apiSecret"]
23
+ }
24
+ }
25
+ }
26
+ }
27
+ }
package/package.json CHANGED
@@ -1,60 +1,60 @@
1
- {
2
- "name": "@eclaw/openclaw-channel",
3
- "version": "1.1.0",
4
- "description": "E-Claw channel plugin for OpenClaw — AI Agent collaboration and A2A communication platform",
5
- "type": "module",
6
- "main": "./dist/index.js",
7
- "types": "./dist/index.d.ts",
8
- "exports": {
9
- ".": {
10
- "import": "./dist/index.js",
11
- "types": "./dist/index.d.ts"
12
- }
13
- },
14
- "files": [
15
- "dist/",
16
- "openclaw.plugin.json",
17
- "README.md"
18
- ],
19
- "scripts": {
20
- "build": "tsc",
21
- "dev": "tsc --watch",
22
- "test": "vitest run",
23
- "lint": "tsc --noEmit",
24
- "prepublishOnly": "npm run build"
25
- },
26
- "openclaw": {
27
- "extensions": [
28
- "./dist/index.js"
29
- ],
30
- "channel": {
31
- "id": "eclaw",
32
- "label": "E-Claw",
33
- "selectionLabel": "E-Claw (AI Agent Collaboration)",
34
- "docsPath": "https://github.com/HankHuang0516/openclaw-channel-eclaw#readme",
35
- "description": "Connect OpenClaw to E-Claw — the AI Agent collaboration and A2A communication platform for Android."
36
- },
37
- "install": {
38
- "npmSpec": "@eclaw/openclaw-channel"
39
- }
40
- },
41
- "keywords": [
42
- "openclaw",
43
- "openclaw-channel",
44
- "channel",
45
- "eclaw",
46
- "ai-agent",
47
- "live-wallpaper"
48
- ],
49
- "author": "HankHuang",
50
- "license": "MIT",
51
- "repository": {
52
- "type": "git",
53
- "url": "git+https://github.com/HankHuang0516/openclaw-channel-eclaw.git"
54
- },
55
- "devDependencies": {
56
- "typescript": "^5.4",
57
- "vitest": "^2.0",
58
- "@types/node": "^20"
59
- }
60
- }
1
+ {
2
+ "name": "@eclaw/openclaw-channel",
3
+ "version": "1.1.1",
4
+ "description": "E-Claw channel plugin for OpenClaw — AI chat platform for live wallpaper entities",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist/",
16
+ "openclaw.plugin.json",
17
+ "README.md"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsc",
21
+ "dev": "tsc --watch",
22
+ "test": "vitest run",
23
+ "lint": "tsc --noEmit",
24
+ "prepublishOnly": "npm run build"
25
+ },
26
+ "openclaw": {
27
+ "extensions": [
28
+ "./dist/index.js"
29
+ ],
30
+ "channel": {
31
+ "id": "eclaw",
32
+ "label": "E-Claw",
33
+ "selectionLabel": "E-Claw (AI Live Wallpaper Chat)",
34
+ "docsPath": "https://github.com/HankHuang0516/openclaw-channel-eclaw#readme",
35
+ "description": "Connect OpenClaw to E-Claw — an AI chat platform for live wallpaper entities on Android."
36
+ },
37
+ "install": {
38
+ "npmSpec": "@eclaw/openclaw-channel"
39
+ }
40
+ },
41
+ "keywords": [
42
+ "openclaw",
43
+ "openclaw-channel",
44
+ "channel",
45
+ "eclaw",
46
+ "ai-agent",
47
+ "live-wallpaper"
48
+ ],
49
+ "author": "HankHuang",
50
+ "license": "MIT",
51
+ "repository": {
52
+ "type": "git",
53
+ "url": "https://github.com/HankHuang0516/openclaw-channel-eclaw"
54
+ },
55
+ "devDependencies": {
56
+ "typescript": "^5.4",
57
+ "vitest": "^2.0",
58
+ "@types/node": "^20"
59
+ }
60
+ }