@eclaw/openclaw-channel 1.0.18 → 1.1.0
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 +58 -155
- package/dist/channel.d.ts +0 -19
- package/dist/channel.js +2 -4
- package/dist/client.d.ts +5 -10
- package/dist/client.js +14 -57
- package/dist/config.js +4 -16
- package/dist/gateway.d.ts +6 -5
- package/dist/gateway.js +165 -89
- package/dist/index.d.ts +21 -0
- package/dist/index.js +6 -35
- package/dist/outbound.d.ts +0 -2
- package/dist/outbound.js +0 -18
- package/dist/types.d.ts +2 -19
- package/dist/webhook-handler.d.ts +3 -14
- package/dist/webhook-handler.js +29 -111
- package/openclaw.plugin.json +27 -27
- package/package.json +60 -60
- package/dist/onboarding.d.ts +0 -19
- package/dist/onboarding.js +0 -77
- package/dist/webhook-registry.d.ts +0 -19
- package/dist/webhook-registry.js +0 -39
package/dist/gateway.js
CHANGED
|
@@ -1,57 +1,51 @@
|
|
|
1
|
+
import { createServer } from 'node:http';
|
|
1
2
|
import { randomBytes } from 'node:crypto';
|
|
2
|
-
import { readFileSync } from 'node:fs';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
import { homedir } from 'node:os';
|
|
5
3
|
import { resolveAccount } from './config.js';
|
|
6
4
|
import { EClawClient } from './client.js';
|
|
7
5
|
import { setClient } from './outbound.js';
|
|
8
6
|
import { createWebhookHandler } from './webhook-handler.js';
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
try {
|
|
35
|
-
fullConfig = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
36
|
-
}
|
|
37
|
-
catch { /* ignore */ }
|
|
38
|
-
return resolveAccount(fullConfig, ctx.accountId ?? ctx.account?.accountId);
|
|
7
|
+
// ── Reconnect / health-check constants ───────────────────────────────────────
|
|
8
|
+
const HEALTH_CHECK_INTERVAL_MS = 60_000; // re-register every 60 s to stay live
|
|
9
|
+
const BACKOFF_INITIAL_MS = 5_000; // first retry after 5 s
|
|
10
|
+
const BACKOFF_MAX_MS = 300_000; // cap at 5 min
|
|
11
|
+
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
|
+
/** Sleep ms, but resolve early if abortSignal fires. */
|
|
19
|
+
function sleep(ms, signal) {
|
|
20
|
+
return new Promise((resolve) => {
|
|
21
|
+
if (signal?.aborted) {
|
|
22
|
+
resolve();
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const timer = setTimeout(resolve, ms);
|
|
26
|
+
signal?.addEventListener('abort', () => { clearTimeout(timer); resolve(); }, { once: true });
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
/** Add ±20 % random jitter to reduce thundering-herd on reconnect. */
|
|
30
|
+
function jitter(ms) {
|
|
31
|
+
return Math.floor(ms * (0.8 + Math.random() * 0.4));
|
|
39
32
|
}
|
|
40
33
|
/**
|
|
41
34
|
* Gateway lifecycle: start an E-Claw account.
|
|
42
35
|
*
|
|
43
|
-
* 1.
|
|
44
|
-
* 2.
|
|
45
|
-
*
|
|
46
|
-
* 3. Register callback URL with E-Claw backend
|
|
36
|
+
* 1. Initialize HTTP client with channel API credentials
|
|
37
|
+
* 2. Start a local HTTP server to receive webhook callbacks
|
|
38
|
+
* 3. Register callback URL with E-Claw backend (with exponential-backoff retry)
|
|
47
39
|
* 4. Auto-bind entity if not already bound
|
|
48
|
-
* 5.
|
|
40
|
+
* 5. Periodically re-register to keep callback URL live (health check)
|
|
41
|
+
* 6. On health-check failure, reconnect with exponential backoff
|
|
42
|
+
* 7. Keep the promise alive until abort signal fires
|
|
49
43
|
*/
|
|
50
44
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
51
45
|
export async function startAccount(ctx) {
|
|
52
|
-
const accountId
|
|
53
|
-
const account =
|
|
54
|
-
if (!account.enabled || !account.apiKey) {
|
|
46
|
+
const { accountId, config } = ctx;
|
|
47
|
+
const account = resolveAccount(config, accountId);
|
|
48
|
+
if (!account.enabled || !account.apiKey || !account.apiSecret) {
|
|
55
49
|
console.log(`[E-Claw] Account ${accountId} disabled or missing credentials, skipping`);
|
|
56
50
|
return;
|
|
57
51
|
}
|
|
@@ -60,59 +54,141 @@ export async function startAccount(ctx) {
|
|
|
60
54
|
setClient(accountId, client);
|
|
61
55
|
// Generate per-session callback token
|
|
62
56
|
const callbackToken = randomBytes(32).toString('hex');
|
|
63
|
-
//
|
|
64
|
-
const
|
|
65
|
-
|
|
57
|
+
// Determine webhook configuration
|
|
58
|
+
const webhookPort = parseInt(process.env.ECLAW_WEBHOOK_PORT || '0') || 0;
|
|
59
|
+
const publicUrl = process.env.ECLAW_WEBHOOK_URL;
|
|
66
60
|
if (!publicUrl) {
|
|
67
|
-
console.warn('[E-Claw]
|
|
68
|
-
'
|
|
69
|
-
'or set ECLAW_WEBHOOK_URL env var. ' +
|
|
70
|
-
'Example: https://your-openclaw-domain.com');
|
|
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');
|
|
71
63
|
}
|
|
72
|
-
//
|
|
73
|
-
const
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const assignedEntityId = bindData.entityId;
|
|
91
|
-
const entityInfo = regData.entities.find(e => e.entityId === assignedEntityId);
|
|
92
|
-
const wasAlreadyBound = entityInfo?.isBound ?? false;
|
|
93
|
-
console.log(wasAlreadyBound
|
|
94
|
-
? `[E-Claw] Entity ${assignedEntityId} reconnected (existing channel binding), publicCode: ${bindData.publicCode}`
|
|
95
|
-
: `[E-Claw] Entity ${assignedEntityId} bound, publicCode: ${bindData.publicCode}`);
|
|
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
|
|
104
|
-
return new Promise((resolve) => {
|
|
105
|
-
const signal = ctx.abortSignal;
|
|
106
|
-
if (signal) {
|
|
107
|
-
signal.addEventListener('abort', () => {
|
|
108
|
-
console.log(`[E-Claw] Shutting down account ${accountId}`);
|
|
109
|
-
client.unregisterCallback().catch(() => { });
|
|
110
|
-
unregisterWebhookToken(callbackToken);
|
|
111
|
-
resolve();
|
|
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);
|
|
112
82
|
});
|
|
113
83
|
}
|
|
114
84
|
else {
|
|
115
|
-
|
|
85
|
+
res.writeHead(404);
|
|
86
|
+
res.end('Not Found');
|
|
116
87
|
}
|
|
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;
|
|
131
|
+
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})...`);
|
|
140
|
+
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;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
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
|
+
});
|
|
117
193
|
});
|
|
118
194
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,24 @@
|
|
|
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
|
+
*/
|
|
1
22
|
declare const plugin: {
|
|
2
23
|
id: string;
|
|
3
24
|
name: string;
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { setPluginRuntime } from './runtime.js';
|
|
2
2
|
import { eclawChannel } from './channel.js';
|
|
3
|
-
import { dispatchWebhook } from './webhook-registry.js';
|
|
4
3
|
/**
|
|
5
4
|
* E-Claw Channel Plugin for OpenClaw.
|
|
6
5
|
*
|
|
@@ -13,52 +12,24 @@ import { dispatchWebhook } from './webhook-registry.js';
|
|
|
13
12
|
* accounts:
|
|
14
13
|
* default:
|
|
15
14
|
* 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"
|
|
20
19
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
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)
|
|
24
23
|
*/
|
|
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
|
-
}
|
|
42
24
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
43
25
|
const plugin = {
|
|
44
|
-
id: '
|
|
26
|
+
id: 'eclaw',
|
|
45
27
|
name: 'E-Claw',
|
|
46
|
-
description: 'E-Claw AI
|
|
28
|
+
description: 'E-Claw AI Agent collaboration channel plugin',
|
|
47
29
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
48
30
|
register(api) {
|
|
49
31
|
console.log('[E-Claw] Plugin loaded');
|
|
50
32
|
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
|
-
});
|
|
62
33
|
api.registerChannel({ plugin: eclawChannel });
|
|
63
34
|
},
|
|
64
35
|
};
|
package/dist/outbound.d.ts
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import { EClawClient } from './client.js';
|
|
2
|
-
export declare function setActiveEvent(accountId: string, event: string): void;
|
|
3
|
-
export declare function clearActiveEvent(accountId: string): void;
|
|
4
2
|
export declare function setClient(accountId: string, client: EClawClient): void;
|
|
5
3
|
export declare function getClient(accountId: string): EClawClient | undefined;
|
|
6
4
|
/** OpenClaw outbound: send text message to E-Claw user */
|
package/dist/outbound.js
CHANGED
|
@@ -1,13 +1,5 @@
|
|
|
1
1
|
/** Client instances keyed by accountId */
|
|
2
2
|
const clients = new Map();
|
|
3
|
-
/** Track current inbound event type per account to suppress duplicate sendMessage calls */
|
|
4
|
-
const activeEvent = new Map();
|
|
5
|
-
export function setActiveEvent(accountId, event) {
|
|
6
|
-
activeEvent.set(accountId, event);
|
|
7
|
-
}
|
|
8
|
-
export function clearActiveEvent(accountId) {
|
|
9
|
-
activeEvent.delete(accountId);
|
|
10
|
-
}
|
|
11
3
|
export function setClient(accountId, client) {
|
|
12
4
|
clients.set(accountId, client);
|
|
13
5
|
}
|
|
@@ -18,11 +10,6 @@ export function getClient(accountId) {
|
|
|
18
10
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
19
11
|
export async function sendText(ctx) {
|
|
20
12
|
const accountId = ctx.accountId ?? 'default';
|
|
21
|
-
// Suppress duplicate delivery for bot-to-bot events — webhook-handler's deliver handles these
|
|
22
|
-
const event = activeEvent.get(accountId) ?? 'message';
|
|
23
|
-
if (event === 'entity_message' || event === 'broadcast') {
|
|
24
|
-
return { channel: 'eclaw', messageId: '', chatId: '' };
|
|
25
|
-
}
|
|
26
13
|
const client = clients.get(accountId);
|
|
27
14
|
if (!client) {
|
|
28
15
|
return { channel: 'eclaw', messageId: '', chatId: '' };
|
|
@@ -45,11 +32,6 @@ export async function sendText(ctx) {
|
|
|
45
32
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
46
33
|
export async function sendMedia(ctx) {
|
|
47
34
|
const accountId = ctx.accountId ?? 'default';
|
|
48
|
-
// Suppress duplicate delivery for bot-to-bot events — webhook-handler's deliver handles these
|
|
49
|
-
const event = activeEvent.get(accountId) ?? 'message';
|
|
50
|
-
if (event === 'entity_message' || event === 'broadcast') {
|
|
51
|
-
return { channel: 'eclaw', messageId: '', chatId: '' };
|
|
52
|
-
}
|
|
53
35
|
const client = clients.get(accountId);
|
|
54
36
|
if (!client) {
|
|
55
37
|
return { channel: 'eclaw', messageId: '', chatId: '' };
|
package/dist/types.d.ts
CHANGED
|
@@ -2,19 +2,10 @@
|
|
|
2
2
|
export interface EClawAccountConfig {
|
|
3
3
|
enabled: boolean;
|
|
4
4
|
apiKey: string;
|
|
5
|
-
apiSecret
|
|
5
|
+
apiSecret: string;
|
|
6
6
|
apiBase: string;
|
|
7
|
-
entityId
|
|
7
|
+
entityId: number;
|
|
8
8
|
botName?: string;
|
|
9
|
-
webhookUrl?: string;
|
|
10
|
-
}
|
|
11
|
-
/** Context block injected by E-Claw server for Channel Bot parity with Traditional Bot */
|
|
12
|
-
export interface EClawContext {
|
|
13
|
-
b2bRemaining?: number;
|
|
14
|
-
b2bMax?: number;
|
|
15
|
-
expectsReply?: boolean;
|
|
16
|
-
missionHints?: string;
|
|
17
|
-
silentToken?: string;
|
|
18
9
|
}
|
|
19
10
|
/** Inbound message from E-Claw callback webhook */
|
|
20
11
|
export interface EClawInboundMessage {
|
|
@@ -33,7 +24,6 @@ export interface EClawInboundMessage {
|
|
|
33
24
|
fromEntityId?: number;
|
|
34
25
|
fromCharacter?: string;
|
|
35
26
|
fromPublicCode?: string;
|
|
36
|
-
eclaw_context?: EClawContext;
|
|
37
27
|
}
|
|
38
28
|
/** Entity info returned by channel register */
|
|
39
29
|
export interface EClawEntityInfo {
|
|
@@ -59,13 +49,6 @@ export interface BindResponse {
|
|
|
59
49
|
publicCode: string;
|
|
60
50
|
bindingType: string;
|
|
61
51
|
}
|
|
62
|
-
/** Error response when all entity slots are full */
|
|
63
|
-
export interface SlotsFullError {
|
|
64
|
-
success: false;
|
|
65
|
-
message: string;
|
|
66
|
-
entities: EClawEntityInfo[];
|
|
67
|
-
hint: string;
|
|
68
|
-
}
|
|
69
52
|
/** Response from POST /api/channel/message */
|
|
70
53
|
export interface MessageResponse {
|
|
71
54
|
success: boolean;
|
|
@@ -1,18 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Create an HTTP request handler for inbound messages from E-Claw.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* - 'entity_message' → Bot-to-bot speak-to; reply via sendMessage() + speakTo(fromEntityId)
|
|
7
|
-
* - 'broadcast' → Broadcast from another entity; reply via sendMessage() + speakTo(fromEntityId)
|
|
8
|
-
*
|
|
9
|
-
* The `deliver` callback routes AI response to the correct E-Claw endpoint
|
|
10
|
-
* based on the inbound event type.
|
|
11
|
-
*
|
|
12
|
-
* Channel Bot Context Parity v1.0.17:
|
|
13
|
-
* - Bot-to-bot / broadcast now calls sendMessage() to update own wallpaper AND speakTo() to reply
|
|
14
|
-
* - Quota awareness via eclaw_context.b2bRemaining / b2bMax
|
|
15
|
-
* - Mission context via eclaw_context.missionHints
|
|
16
|
-
* - Silent suppression via silentToken (default "[SILENT]")
|
|
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.
|
|
17
6
|
*/
|
|
18
|
-
export declare function createWebhookHandler(expectedToken: string, accountId: string
|
|
7
|
+
export declare function createWebhookHandler(expectedToken: string, accountId: string): (req: any, res: any) => Promise<void>;
|
package/dist/webhook-handler.js
CHANGED
|
@@ -1,26 +1,11 @@
|
|
|
1
1
|
import { getPluginRuntime } from './runtime.js';
|
|
2
|
-
import { getClient, setActiveEvent, clearActiveEvent } from './outbound.js';
|
|
3
2
|
/**
|
|
4
3
|
* Create an HTTP request handler for inbound messages from E-Claw.
|
|
5
4
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* - 'entity_message' → Bot-to-bot speak-to; reply via sendMessage() + speakTo(fromEntityId)
|
|
9
|
-
* - 'broadcast' → Broadcast from another entity; reply via sendMessage() + speakTo(fromEntityId)
|
|
10
|
-
*
|
|
11
|
-
* The `deliver` callback routes AI response to the correct E-Claw endpoint
|
|
12
|
-
* based on the inbound event type.
|
|
13
|
-
*
|
|
14
|
-
* Channel Bot Context Parity v1.0.17:
|
|
15
|
-
* - Bot-to-bot / broadcast now calls sendMessage() to update own wallpaper AND speakTo() to reply
|
|
16
|
-
* - Quota awareness via eclaw_context.b2bRemaining / b2bMax
|
|
17
|
-
* - Mission context via eclaw_context.missionHints
|
|
18
|
-
* - Silent suppression via silentToken (default "[SILENT]")
|
|
5
|
+
* 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.
|
|
19
7
|
*/
|
|
20
|
-
export function createWebhookHandler(expectedToken, accountId
|
|
21
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
22
|
-
cfg // full openclaw config (ctx.cfg from startAccount)
|
|
23
|
-
) {
|
|
8
|
+
export function createWebhookHandler(expectedToken, accountId) {
|
|
24
9
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
25
10
|
return async (req, res) => {
|
|
26
11
|
// Verify callback token
|
|
@@ -37,103 +22,36 @@ cfg // full openclaw config (ctx.cfg from startAccount)
|
|
|
37
22
|
// Dispatch to OpenClaw agent
|
|
38
23
|
try {
|
|
39
24
|
const rt = getPluginRuntime();
|
|
40
|
-
const client = getClient(accountId);
|
|
41
25
|
const conversationId = msg.conversationId || `${msg.deviceId}:${msg.entityId}`;
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const ocMediaType = msg.mediaType === 'photo' ? 'image'
|
|
51
|
-
: msg.mediaType === 'voice' ? 'audio'
|
|
52
|
-
: msg.mediaType === 'video' ? 'video'
|
|
53
|
-
: msg.mediaType ? 'file'
|
|
54
|
-
: undefined;
|
|
55
|
-
// Build body — enrich with event context for bot-to-bot and broadcast
|
|
56
|
-
let body = msg.text || '';
|
|
57
|
-
if ((event === 'entity_message' || event === 'broadcast') && fromEntityId !== undefined) {
|
|
58
|
-
const senderLabel = fromCharacter
|
|
59
|
-
? `Entity ${fromEntityId} (${fromCharacter})`
|
|
60
|
-
: `Entity ${fromEntityId}`;
|
|
61
|
-
const eventPrefix = event === 'broadcast'
|
|
62
|
-
? `[Broadcast from ${senderLabel}]`
|
|
63
|
-
: `[Bot-to-Bot message from ${senderLabel}]`;
|
|
64
|
-
const quotaLine = eclawCtx?.b2bRemaining !== undefined
|
|
65
|
-
? `[Quota: ${eclawCtx.b2bRemaining}/${eclawCtx.b2bMax ?? 8} remaining — output "${silentToken}" if no new info worth replying to]`
|
|
66
|
-
: '';
|
|
67
|
-
const missionBlock = eclawCtx?.missionHints ?? '';
|
|
68
|
-
body = [eventPrefix, quotaLine, missionBlock, msg.text || '']
|
|
69
|
-
.filter(Boolean)
|
|
70
|
-
.join('\n');
|
|
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 };
|
|
71
34
|
}
|
|
72
|
-
// Build context in OpenClaw's native PascalCase format
|
|
73
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
74
35
|
const inboundCtx = {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
} : {}),
|
|
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
|
+
},
|
|
91
51
|
};
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
try {
|
|
96
|
-
await rt.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
97
|
-
ctx: ctxPayload,
|
|
98
|
-
cfg,
|
|
99
|
-
dispatcherOptions: {
|
|
100
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
101
|
-
deliver: async (payload) => {
|
|
102
|
-
if (!client)
|
|
103
|
-
return;
|
|
104
|
-
const text = typeof payload.text === 'string' ? payload.text.trim() : '';
|
|
105
|
-
// [SILENT] token or empty → skip all API calls
|
|
106
|
-
if (!text || text === silentToken)
|
|
107
|
-
return;
|
|
108
|
-
if ((event === 'entity_message' || event === 'broadcast') && fromEntityId !== undefined) {
|
|
109
|
-
// Bot-to-bot / broadcast: update own wallpaper AND reply to sender
|
|
110
|
-
await client.sendMessage(text, 'IDLE');
|
|
111
|
-
await client.speakTo(fromEntityId, text, false);
|
|
112
|
-
}
|
|
113
|
-
else {
|
|
114
|
-
// Normal human message: reply via channel message
|
|
115
|
-
if (text) {
|
|
116
|
-
await client.sendMessage(text, 'IDLE');
|
|
117
|
-
}
|
|
118
|
-
else if (payload.mediaUrl) {
|
|
119
|
-
const rawType = typeof payload.mediaType === 'string' ? payload.mediaType : '';
|
|
120
|
-
const mediaType = rawType === 'image' ? 'photo'
|
|
121
|
-
: rawType === 'audio' ? 'voice'
|
|
122
|
-
: rawType === 'video' ? 'video'
|
|
123
|
-
: 'file';
|
|
124
|
-
await client.sendMessage('', 'IDLE', mediaType, payload.mediaUrl);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
},
|
|
128
|
-
onError: (err) => {
|
|
129
|
-
console.error('[E-Claw] Reply delivery error:', err);
|
|
130
|
-
},
|
|
131
|
-
},
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
finally {
|
|
135
|
-
clearActiveEvent(accountId);
|
|
136
|
-
}
|
|
52
|
+
// OpenClaw inbound dispatch pipeline
|
|
53
|
+
const ctx = await rt.channel.reply.finalizeInboundContext(inboundCtx);
|
|
54
|
+
await rt.channel.reply.dispatchReplyWithBufferedBlockDispatcher(ctx);
|
|
137
55
|
}
|
|
138
56
|
catch (err) {
|
|
139
57
|
console.error('[E-Claw] Webhook dispatch error:', err);
|