@eclaw/openclaw-channel 1.0.18 → 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/dist/client.d.ts +4 -10
- package/dist/client.js +9 -57
- package/dist/config.js +1 -1
- package/dist/gateway.d.ts +4 -2
- package/dist/gateway.js +108 -25
- package/dist/outbound.d.ts +0 -2
- package/dist/outbound.js +0 -18
- package/dist/types.d.ts +1 -17
- package/dist/webhook-handler.d.ts +4 -12
- package/dist/webhook-handler.js +34 -83
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -12,19 +12,13 @@ export declare class EClawClient {
|
|
|
12
12
|
constructor(config: EClawAccountConfig);
|
|
13
13
|
/** Register callback URL with E-Claw backend */
|
|
14
14
|
registerCallback(callbackUrl: string, callbackToken: string): Promise<RegisterResponse>;
|
|
15
|
-
/** Bind an entity via channel API (bypasses 6-digit code)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
bindEntity(entityId?: number, name?: string): Promise<BindResponse>;
|
|
19
|
-
/** Send bot message to user (updates own entity state on wallpaper) */
|
|
15
|
+
/** Bind an entity via channel API (bypasses 6-digit code) */
|
|
16
|
+
bindEntity(entityId: number, name?: string): Promise<BindResponse>;
|
|
17
|
+
/** Send bot message to user */
|
|
20
18
|
sendMessage(message: string, state?: string, mediaType?: string, mediaUrl?: string): Promise<MessageResponse>;
|
|
21
|
-
/** Send bot-to-bot message to another entity (speak-to) */
|
|
22
|
-
speakTo(toEntityId: number, text: string, expectsReply?: boolean): Promise<void>;
|
|
23
|
-
/** Broadcast message to all other bound entities */
|
|
24
|
-
broadcastToAll(text: string, expectsReply?: boolean): Promise<void>;
|
|
25
19
|
/** Unregister callback on shutdown */
|
|
26
20
|
unregisterCallback(): Promise<void>;
|
|
27
21
|
get currentDeviceId(): string | null;
|
|
28
22
|
get currentBotSecret(): string | null;
|
|
29
|
-
get currentEntityId(): number
|
|
23
|
+
get currentEntityId(): number;
|
|
30
24
|
}
|
package/dist/client.js
CHANGED
|
@@ -11,7 +11,7 @@ export class EClawClient {
|
|
|
11
11
|
constructor(config) {
|
|
12
12
|
this.apiBase = config.apiBase;
|
|
13
13
|
this.apiKey = config.apiKey;
|
|
14
|
-
this.entityId = config.entityId;
|
|
14
|
+
this.entityId = config.entityId;
|
|
15
15
|
}
|
|
16
16
|
/** Register callback URL with E-Claw backend */
|
|
17
17
|
async registerCallback(callbackUrl, callbackToken) {
|
|
@@ -31,40 +31,27 @@ export class EClawClient {
|
|
|
31
31
|
this.deviceId = data.deviceId;
|
|
32
32
|
return data;
|
|
33
33
|
}
|
|
34
|
-
/** Bind an entity via channel API (bypasses 6-digit code)
|
|
35
|
-
* If entityId is omitted, the backend auto-selects the first free slot.
|
|
36
|
-
*/
|
|
34
|
+
/** Bind an entity via channel API (bypasses 6-digit code) */
|
|
37
35
|
async bindEntity(entityId, name) {
|
|
38
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
39
|
-
const body = { channel_api_key: this.apiKey };
|
|
40
|
-
if (entityId !== undefined)
|
|
41
|
-
body.entityId = entityId;
|
|
42
|
-
if (name)
|
|
43
|
-
body.name = name;
|
|
44
36
|
const res = await fetch(`${this.apiBase}/api/channel/bind`, {
|
|
45
37
|
method: 'POST',
|
|
46
38
|
headers: { 'Content-Type': 'application/json' },
|
|
47
|
-
body: JSON.stringify(
|
|
39
|
+
body: JSON.stringify({
|
|
40
|
+
channel_api_key: this.apiKey,
|
|
41
|
+
entityId,
|
|
42
|
+
name: name || undefined,
|
|
43
|
+
}),
|
|
48
44
|
});
|
|
49
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
50
45
|
const data = await res.json();
|
|
51
46
|
if (!data.success) {
|
|
52
|
-
// Build a detailed error message when all slots are full
|
|
53
|
-
if (res.status === 409 && data.entities) {
|
|
54
|
-
const list = data.entities
|
|
55
|
-
.map((e) => ` slot ${e.entityId} (${e.character})${e.name ? ` "${e.name}"` : ''}`)
|
|
56
|
-
.join('\n');
|
|
57
|
-
throw new Error(`${data.message}\nCurrent entities:\n${list}\n` +
|
|
58
|
-
'Add entityId to your channel config to target a specific slot after unbinding it.');
|
|
59
|
-
}
|
|
60
47
|
throw new Error(data.message || `Bind failed (HTTP ${res.status})`);
|
|
61
48
|
}
|
|
62
49
|
this.botSecret = data.botSecret;
|
|
63
50
|
this.deviceId = data.deviceId;
|
|
64
|
-
this.entityId =
|
|
51
|
+
this.entityId = entityId;
|
|
65
52
|
return data;
|
|
66
53
|
}
|
|
67
|
-
/** Send bot message to user
|
|
54
|
+
/** Send bot message to user */
|
|
68
55
|
async sendMessage(message, state = 'IDLE', mediaType, mediaUrl) {
|
|
69
56
|
if (!this.deviceId || !this.botSecret) {
|
|
70
57
|
throw new Error('Not bound — call bindEntity() first');
|
|
@@ -85,41 +72,6 @@ export class EClawClient {
|
|
|
85
72
|
});
|
|
86
73
|
return await res.json();
|
|
87
74
|
}
|
|
88
|
-
/** Send bot-to-bot message to another entity (speak-to) */
|
|
89
|
-
async speakTo(toEntityId, text, expectsReply = false) {
|
|
90
|
-
if (!this.deviceId || !this.botSecret) {
|
|
91
|
-
throw new Error('Not bound — call bindEntity() first');
|
|
92
|
-
}
|
|
93
|
-
await fetch(`${this.apiBase}/api/entity/speak-to`, {
|
|
94
|
-
method: 'POST',
|
|
95
|
-
headers: { 'Content-Type': 'application/json' },
|
|
96
|
-
body: JSON.stringify({
|
|
97
|
-
deviceId: this.deviceId,
|
|
98
|
-
fromEntityId: this.entityId,
|
|
99
|
-
toEntityId,
|
|
100
|
-
botSecret: this.botSecret,
|
|
101
|
-
text,
|
|
102
|
-
expects_reply: expectsReply,
|
|
103
|
-
}),
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
/** Broadcast message to all other bound entities */
|
|
107
|
-
async broadcastToAll(text, expectsReply = false) {
|
|
108
|
-
if (!this.deviceId || !this.botSecret) {
|
|
109
|
-
throw new Error('Not bound — call bindEntity() first');
|
|
110
|
-
}
|
|
111
|
-
await fetch(`${this.apiBase}/api/entity/broadcast`, {
|
|
112
|
-
method: 'POST',
|
|
113
|
-
headers: { 'Content-Type': 'application/json' },
|
|
114
|
-
body: JSON.stringify({
|
|
115
|
-
deviceId: this.deviceId,
|
|
116
|
-
fromEntityId: this.entityId,
|
|
117
|
-
botSecret: this.botSecret,
|
|
118
|
-
text,
|
|
119
|
-
expects_reply: expectsReply,
|
|
120
|
-
}),
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
75
|
/** Unregister callback on shutdown */
|
|
124
76
|
async unregisterCallback() {
|
|
125
77
|
await fetch(`${this.apiBase}/api/channel/register`, {
|
package/dist/config.js
CHANGED
|
@@ -28,7 +28,7 @@ export function resolveAccount(cfg, accountId) {
|
|
|
28
28
|
apiKey: account?.apiKey ?? '',
|
|
29
29
|
apiSecret: account?.apiSecret,
|
|
30
30
|
apiBase: (account?.apiBase ?? 'https://eclawbot.com').replace(/\/$/, ''),
|
|
31
|
-
entityId: account?.entityId
|
|
31
|
+
entityId: account?.entityId ?? 0,
|
|
32
32
|
botName: account?.botName,
|
|
33
33
|
webhookUrl: account?.webhookUrl,
|
|
34
34
|
};
|
package/dist/gateway.d.ts
CHANGED
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
* 1. Resolve credentials from ctx.account or disk
|
|
5
5
|
* 2. Register a per-session handler in the webhook-registry (served by the
|
|
6
6
|
* main OpenClaw gateway HTTP server at /eclaw-webhook — no separate port)
|
|
7
|
-
* 3. Register callback URL with E-Claw backend
|
|
7
|
+
* 3. Register callback URL with E-Claw backend (with exponential-backoff retry)
|
|
8
8
|
* 4. Auto-bind entity if not already bound
|
|
9
|
-
* 5.
|
|
9
|
+
* 5. Periodically re-register to keep callback URL live (health check)
|
|
10
|
+
* 6. On health-check failure, reconnect with exponential backoff
|
|
11
|
+
* 7. Keep the promise alive until abort signal fires
|
|
10
12
|
*/
|
|
11
13
|
export declare function startAccount(ctx: any): Promise<void>;
|
package/dist/gateway.js
CHANGED
|
@@ -7,6 +7,26 @@ import { EClawClient } from './client.js';
|
|
|
7
7
|
import { setClient } from './outbound.js';
|
|
8
8
|
import { createWebhookHandler } from './webhook-handler.js';
|
|
9
9
|
import { registerWebhookToken, unregisterWebhookToken } from './webhook-registry.js';
|
|
10
|
+
// ── Reconnect / health-check constants ───────────────────────────────────────
|
|
11
|
+
const HEALTH_CHECK_INTERVAL_MS = 60_000; // re-register every 60 s to stay live
|
|
12
|
+
const BACKOFF_INITIAL_MS = 5_000; // first retry after 5 s
|
|
13
|
+
const BACKOFF_MAX_MS = 300_000; // cap at 5 min
|
|
14
|
+
const BACKOFF_MULTIPLIER = 2;
|
|
15
|
+
/** Sleep ms, but resolve early if abortSignal fires. */
|
|
16
|
+
function sleep(ms, signal) {
|
|
17
|
+
return new Promise((resolve) => {
|
|
18
|
+
if (signal?.aborted) {
|
|
19
|
+
resolve();
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const timer = setTimeout(resolve, ms);
|
|
23
|
+
signal?.addEventListener('abort', () => { clearTimeout(timer); resolve(); }, { once: true });
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
/** Add ±20 % random jitter to reduce thundering-herd on reconnect. */
|
|
27
|
+
function jitter(ms) {
|
|
28
|
+
return Math.floor(ms * (0.8 + Math.random() * 0.4));
|
|
29
|
+
}
|
|
10
30
|
/**
|
|
11
31
|
* Resolve account from ctx.
|
|
12
32
|
*
|
|
@@ -22,7 +42,7 @@ function resolveAccountFromCtx(ctx) {
|
|
|
22
42
|
apiKey: ctx.account.apiKey,
|
|
23
43
|
apiSecret: ctx.account.apiSecret,
|
|
24
44
|
apiBase: (ctx.account.apiBase ?? 'https://eclawbot.com').replace(/\/$/, ''),
|
|
25
|
-
entityId: ctx.account.entityId
|
|
45
|
+
entityId: ctx.account.entityId ?? 0,
|
|
26
46
|
botName: ctx.account.botName,
|
|
27
47
|
webhookUrl: ctx.account.webhookUrl,
|
|
28
48
|
};
|
|
@@ -43,9 +63,11 @@ function resolveAccountFromCtx(ctx) {
|
|
|
43
63
|
* 1. Resolve credentials from ctx.account or disk
|
|
44
64
|
* 2. Register a per-session handler in the webhook-registry (served by the
|
|
45
65
|
* main OpenClaw gateway HTTP server at /eclaw-webhook — no separate port)
|
|
46
|
-
* 3. Register callback URL with E-Claw backend
|
|
66
|
+
* 3. Register callback URL with E-Claw backend (with exponential-backoff retry)
|
|
47
67
|
* 4. Auto-bind entity if not already bound
|
|
48
|
-
* 5.
|
|
68
|
+
* 5. Periodically re-register to keep callback URL live (health check)
|
|
69
|
+
* 6. On health-check failure, reconnect with exponential backoff
|
|
70
|
+
* 7. Keep the promise alive until abort signal fires
|
|
49
71
|
*/
|
|
50
72
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
51
73
|
export async function startAccount(ctx) {
|
|
@@ -76,42 +98,103 @@ export async function startAccount(ctx) {
|
|
|
76
98
|
const handler = createWebhookHandler(callbackToken, accountId, ctx.cfg);
|
|
77
99
|
registerWebhookToken(callbackToken, accountId, handler);
|
|
78
100
|
console.log(`[E-Claw] Webhook registered at: ${callbackUrl}`);
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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;
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
console.error(`[E-Claw][${accountId}] Setup attempt failed:`, err);
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
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);
|
|
97
143
|
}
|
|
98
|
-
|
|
99
|
-
console.error(`[E-Claw] Setup failed for account ${accountId}:`, err);
|
|
144
|
+
if (signal?.aborted) {
|
|
100
145
|
unregisterWebhookToken(callbackToken);
|
|
101
146
|
return;
|
|
102
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;
|
|
166
|
+
while (!signal?.aborted) {
|
|
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...`);
|
|
170
|
+
await sleep(delay, signal);
|
|
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;
|
|
177
|
+
}
|
|
178
|
+
reconnBackoff = Math.min(reconnBackoff * BACKOFF_MULTIPLIER, BACKOFF_MAX_MS);
|
|
179
|
+
}
|
|
180
|
+
isReconnecting = false;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const healthTimer = setInterval(() => { void runHealthCheck(); }, HEALTH_CHECK_INTERVAL_MS);
|
|
103
184
|
// Keep the promise alive until abort signal fires
|
|
104
185
|
return new Promise((resolve) => {
|
|
105
|
-
const signal = ctx.abortSignal;
|
|
106
186
|
if (signal) {
|
|
107
187
|
signal.addEventListener('abort', () => {
|
|
108
|
-
console.log(`[E-Claw] Shutting down account
|
|
188
|
+
console.log(`[E-Claw][${accountId}] Shutting down account`);
|
|
189
|
+
clearInterval(healthTimer);
|
|
109
190
|
client.unregisterCallback().catch(() => { });
|
|
110
191
|
unregisterWebhookToken(callbackToken);
|
|
111
192
|
resolve();
|
|
112
193
|
});
|
|
113
194
|
}
|
|
114
195
|
else {
|
|
196
|
+
// No abort signal — resolve immediately (should not happen in normal use)
|
|
197
|
+
clearInterval(healthTimer);
|
|
115
198
|
resolve();
|
|
116
199
|
}
|
|
117
200
|
});
|
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
|
@@ -4,18 +4,10 @@ export interface EClawAccountConfig {
|
|
|
4
4
|
apiKey: string;
|
|
5
5
|
apiSecret?: string;
|
|
6
6
|
apiBase: string;
|
|
7
|
-
entityId
|
|
7
|
+
entityId: number;
|
|
8
8
|
botName?: string;
|
|
9
9
|
webhookUrl?: string;
|
|
10
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
|
-
}
|
|
19
11
|
/** Inbound message from E-Claw callback webhook */
|
|
20
12
|
export interface EClawInboundMessage {
|
|
21
13
|
event: 'message' | 'entity_message' | 'broadcast' | 'cross_device_message';
|
|
@@ -33,7 +25,6 @@ export interface EClawInboundMessage {
|
|
|
33
25
|
fromEntityId?: number;
|
|
34
26
|
fromCharacter?: string;
|
|
35
27
|
fromPublicCode?: string;
|
|
36
|
-
eclaw_context?: EClawContext;
|
|
37
28
|
}
|
|
38
29
|
/** Entity info returned by channel register */
|
|
39
30
|
export interface EClawEntityInfo {
|
|
@@ -59,13 +50,6 @@ export interface BindResponse {
|
|
|
59
50
|
publicCode: string;
|
|
60
51
|
bindingType: string;
|
|
61
52
|
}
|
|
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
53
|
/** Response from POST /api/channel/message */
|
|
70
54
|
export interface MessageResponse {
|
|
71
55
|
success: boolean;
|
|
@@ -1,18 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Create an HTTP request handler for inbound messages from E-Claw.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* - 'broadcast' → Broadcast from another entity; reply via sendMessage() + speakTo(fromEntityId)
|
|
4
|
+
* When a user sends a message on E-Claw, the backend POSTs structured JSON
|
|
5
|
+
* to this webhook. We normalize it into OpenClaw's native PascalCase context
|
|
6
|
+
* format and dispatch to the agent via dispatchReplyWithBufferedBlockDispatcher.
|
|
8
7
|
*
|
|
9
|
-
* The `deliver` callback
|
|
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]")
|
|
8
|
+
* The `deliver` callback sends the AI reply back to E-Claw via the API client.
|
|
17
9
|
*/
|
|
18
10
|
export declare function createWebhookHandler(expectedToken: string, accountId: string, cfg: any): (req: any, res: any) => Promise<void>;
|
package/dist/webhook-handler.js
CHANGED
|
@@ -1,21 +1,13 @@
|
|
|
1
1
|
import { getPluginRuntime } from './runtime.js';
|
|
2
|
-
import { getClient
|
|
2
|
+
import { getClient } from './outbound.js';
|
|
3
3
|
/**
|
|
4
4
|
* Create an HTTP request handler for inbound messages from E-Claw.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* - 'broadcast' → Broadcast from another entity; reply via sendMessage() + speakTo(fromEntityId)
|
|
6
|
+
* When a user sends a message on E-Claw, the backend POSTs structured JSON
|
|
7
|
+
* to this webhook. We normalize it into OpenClaw's native PascalCase context
|
|
8
|
+
* format and dispatch to the agent via dispatchReplyWithBufferedBlockDispatcher.
|
|
10
9
|
*
|
|
11
|
-
* The `deliver` callback
|
|
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]")
|
|
10
|
+
* The `deliver` callback sends the AI reply back to E-Claw via the API client.
|
|
19
11
|
*/
|
|
20
12
|
export function createWebhookHandler(expectedToken, accountId,
|
|
21
13
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -39,37 +31,14 @@ cfg // full openclaw config (ctx.cfg from startAccount)
|
|
|
39
31
|
const rt = getPluginRuntime();
|
|
40
32
|
const client = getClient(accountId);
|
|
41
33
|
const conversationId = msg.conversationId || `${msg.deviceId}:${msg.entityId}`;
|
|
42
|
-
// Capture event context for deliver routing
|
|
43
|
-
const event = msg.event || 'message';
|
|
44
|
-
const fromEntityId = msg.fromEntityId;
|
|
45
|
-
const fromCharacter = msg.fromCharacter;
|
|
46
|
-
// Read server-injected context block (Channel Bot parity)
|
|
47
|
-
const eclawCtx = msg.eclaw_context;
|
|
48
|
-
const silentToken = eclawCtx?.silentToken ?? '[SILENT]';
|
|
49
34
|
// Map E-Claw media type to OpenClaw media type
|
|
50
35
|
const ocMediaType = msg.mediaType === 'photo' ? 'image'
|
|
51
36
|
: msg.mediaType === 'voice' ? 'audio'
|
|
52
37
|
: msg.mediaType === 'video' ? 'video'
|
|
53
38
|
: msg.mediaType ? 'file'
|
|
54
39
|
: 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');
|
|
71
|
-
}
|
|
72
40
|
// Build context in OpenClaw's native PascalCase format
|
|
41
|
+
// (same convention as Telegram/LINE/WhatsApp channels)
|
|
73
42
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
74
43
|
const inboundCtx = {
|
|
75
44
|
Surface: 'eclaw',
|
|
@@ -80,9 +49,9 @@ cfg // full openclaw config (ctx.cfg from startAccount)
|
|
|
80
49
|
To: conversationId,
|
|
81
50
|
OriginatingTo: msg.from,
|
|
82
51
|
SessionKey: conversationId,
|
|
83
|
-
Body:
|
|
84
|
-
RawBody:
|
|
85
|
-
CommandBody:
|
|
52
|
+
Body: msg.text || '',
|
|
53
|
+
RawBody: msg.text || '',
|
|
54
|
+
CommandBody: msg.text || '',
|
|
86
55
|
ChatType: 'direct',
|
|
87
56
|
...(ocMediaType && msg.mediaUrl ? {
|
|
88
57
|
MediaType: ocMediaType,
|
|
@@ -90,50 +59,32 @@ cfg // full openclaw config (ctx.cfg from startAccount)
|
|
|
90
59
|
} : {}),
|
|
91
60
|
};
|
|
92
61
|
const ctxPayload = rt.channel.reply.finalizeInboundContext(inboundCtx);
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
},
|
|
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);
|
|
131
85
|
},
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
finally {
|
|
135
|
-
clearActiveEvent(accountId);
|
|
136
|
-
}
|
|
86
|
+
},
|
|
87
|
+
});
|
|
137
88
|
}
|
|
138
89
|
catch (err) {
|
|
139
90
|
console.error('[E-Claw] Webhook dispatch error:', err);
|
package/openclaw.plugin.json
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"apiKey": { "type": "string", "description": "Channel API Key (eck_...)" },
|
|
17
17
|
"apiSecret": { "type": "string", "description": "Channel API Secret (ecs_...)" },
|
|
18
18
|
"apiBase": { "type": "string", "default": "https://eclawbot.com" },
|
|
19
|
-
"entityId": { "type": "number", "
|
|
19
|
+
"entityId": { "type": "number", "default": 0, "minimum": 0, "maximum": 7 },
|
|
20
20
|
"botName": { "type": "string", "maxLength": 20 }
|
|
21
21
|
},
|
|
22
22
|
"required": ["apiKey", "apiSecret"]
|