@eclaw/openclaw-channel 1.0.15 → 1.0.16
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 +5 -1
- package/dist/client.js +36 -1
- package/dist/gateway.js +9 -7
- package/dist/webhook-handler.d.ts +6 -4
- package/dist/webhook-handler.js +43 -17
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -16,8 +16,12 @@ export declare class EClawClient {
|
|
|
16
16
|
* If entityId is omitted, the backend auto-selects the first free slot.
|
|
17
17
|
*/
|
|
18
18
|
bindEntity(entityId?: number, name?: string): Promise<BindResponse>;
|
|
19
|
-
/** Send bot message to user */
|
|
19
|
+
/** Send bot message to user (updates own entity state on wallpaper) */
|
|
20
20
|
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>;
|
|
21
25
|
/** Unregister callback on shutdown */
|
|
22
26
|
unregisterCallback(): Promise<void>;
|
|
23
27
|
get currentDeviceId(): string | null;
|
package/dist/client.js
CHANGED
|
@@ -64,7 +64,7 @@ export class EClawClient {
|
|
|
64
64
|
this.entityId = data.entityId; // Use server-assigned slot
|
|
65
65
|
return data;
|
|
66
66
|
}
|
|
67
|
-
/** Send bot message to user */
|
|
67
|
+
/** Send bot message to user (updates own entity state on wallpaper) */
|
|
68
68
|
async sendMessage(message, state = 'IDLE', mediaType, mediaUrl) {
|
|
69
69
|
if (!this.deviceId || !this.botSecret) {
|
|
70
70
|
throw new Error('Not bound — call bindEntity() first');
|
|
@@ -85,6 +85,41 @@ export class EClawClient {
|
|
|
85
85
|
});
|
|
86
86
|
return await res.json();
|
|
87
87
|
}
|
|
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
|
+
}
|
|
88
123
|
/** Unregister callback on shutdown */
|
|
89
124
|
async unregisterCallback() {
|
|
90
125
|
await fetch(`${this.apiBase}/api/channel/register`, {
|
package/dist/gateway.js
CHANGED
|
@@ -22,7 +22,7 @@ function resolveAccountFromCtx(ctx) {
|
|
|
22
22
|
apiKey: ctx.account.apiKey,
|
|
23
23
|
apiSecret: ctx.account.apiSecret,
|
|
24
24
|
apiBase: (ctx.account.apiBase ?? 'https://eclawbot.com').replace(/\/$/, ''),
|
|
25
|
-
entityId: ctx.account.entityId
|
|
25
|
+
entityId: ctx.account.entityId, // undefined = auto-select
|
|
26
26
|
botName: ctx.account.botName,
|
|
27
27
|
webhookUrl: ctx.account.webhookUrl,
|
|
28
28
|
};
|
|
@@ -83,14 +83,16 @@ export async function startAccount(ctx) {
|
|
|
83
83
|
// Bind entity via channel API.
|
|
84
84
|
// /api/channel/bind is idempotent for the same channel account:
|
|
85
85
|
// - Not bound → binds fresh, returns new botSecret
|
|
86
|
-
// - Already bound via this channel account → returns existing botSecret
|
|
86
|
+
// - Already bound via this channel account → returns existing botSecret (reconnect)
|
|
87
87
|
// - Bound via different method → throws error (user must unbind first)
|
|
88
|
-
|
|
89
|
-
const alreadyBound = entityInfo?.isBound ?? false;
|
|
88
|
+
// entityId is omitted here so the server auto-selects the best slot
|
|
90
89
|
const bindData = await client.bindEntity(account.entityId, account.botName);
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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}`);
|
|
94
96
|
console.log(`[E-Claw] Account ${accountId} ready!`);
|
|
95
97
|
}
|
|
96
98
|
catch (err) {
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Create an HTTP request handler for inbound messages from E-Claw.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Handles three event types:
|
|
5
|
+
* - 'message' → Normal human message; reply via sendMessage()
|
|
6
|
+
* - 'entity_message' → Bot-to-bot speak-to; reply via speakTo(fromEntityId)
|
|
7
|
+
* - 'broadcast' → Broadcast from another entity; reply via speakTo(fromEntityId)
|
|
7
8
|
*
|
|
8
|
-
* The `deliver` callback
|
|
9
|
+
* The `deliver` callback routes AI response to the correct E-Claw endpoint
|
|
10
|
+
* based on the inbound event type.
|
|
9
11
|
*/
|
|
10
12
|
export declare function createWebhookHandler(expectedToken: string, accountId: string, cfg: any): (req: any, res: any) => Promise<void>;
|
package/dist/webhook-handler.js
CHANGED
|
@@ -3,11 +3,13 @@ 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
|
-
*
|
|
6
|
+
* Handles three event types:
|
|
7
|
+
* - 'message' → Normal human message; reply via sendMessage()
|
|
8
|
+
* - 'entity_message' → Bot-to-bot speak-to; reply via speakTo(fromEntityId)
|
|
9
|
+
* - 'broadcast' → Broadcast from another entity; reply via speakTo(fromEntityId)
|
|
9
10
|
*
|
|
10
|
-
* The `deliver` callback
|
|
11
|
+
* The `deliver` callback routes AI response to the correct E-Claw endpoint
|
|
12
|
+
* based on the inbound event type.
|
|
11
13
|
*/
|
|
12
14
|
export function createWebhookHandler(expectedToken, accountId,
|
|
13
15
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -31,14 +33,28 @@ cfg // full openclaw config (ctx.cfg from startAccount)
|
|
|
31
33
|
const rt = getPluginRuntime();
|
|
32
34
|
const client = getClient(accountId);
|
|
33
35
|
const conversationId = msg.conversationId || `${msg.deviceId}:${msg.entityId}`;
|
|
36
|
+
// Capture event context for deliver routing
|
|
37
|
+
const event = msg.event || 'message';
|
|
38
|
+
const fromEntityId = msg.fromEntityId;
|
|
39
|
+
const fromCharacter = msg.fromCharacter;
|
|
34
40
|
// Map E-Claw media type to OpenClaw media type
|
|
35
41
|
const ocMediaType = msg.mediaType === 'photo' ? 'image'
|
|
36
42
|
: msg.mediaType === 'voice' ? 'audio'
|
|
37
43
|
: msg.mediaType === 'video' ? 'video'
|
|
38
44
|
: msg.mediaType ? 'file'
|
|
39
45
|
: undefined;
|
|
46
|
+
// Build body — enrich with event context for bot-to-bot and broadcast
|
|
47
|
+
let body = msg.text || '';
|
|
48
|
+
if ((event === 'entity_message' || event === 'broadcast') && fromEntityId !== undefined) {
|
|
49
|
+
const senderLabel = fromCharacter
|
|
50
|
+
? `Entity ${fromEntityId} (${fromCharacter})`
|
|
51
|
+
: `Entity ${fromEntityId}`;
|
|
52
|
+
const prefix = event === 'broadcast'
|
|
53
|
+
? `[Broadcast from ${senderLabel}]`
|
|
54
|
+
: `[Bot-to-Bot message from ${senderLabel}]`;
|
|
55
|
+
body = `${prefix}\n${msg.text || ''}`;
|
|
56
|
+
}
|
|
40
57
|
// Build context in OpenClaw's native PascalCase format
|
|
41
|
-
// (same convention as Telegram/LINE/WhatsApp channels)
|
|
42
58
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
43
59
|
const inboundCtx = {
|
|
44
60
|
Surface: 'eclaw',
|
|
@@ -49,9 +65,9 @@ cfg // full openclaw config (ctx.cfg from startAccount)
|
|
|
49
65
|
To: conversationId,
|
|
50
66
|
OriginatingTo: msg.from,
|
|
51
67
|
SessionKey: conversationId,
|
|
52
|
-
Body:
|
|
53
|
-
RawBody:
|
|
54
|
-
CommandBody:
|
|
68
|
+
Body: body,
|
|
69
|
+
RawBody: body,
|
|
70
|
+
CommandBody: body,
|
|
55
71
|
ChatType: 'direct',
|
|
56
72
|
...(ocMediaType && msg.mediaUrl ? {
|
|
57
73
|
MediaType: ocMediaType,
|
|
@@ -68,16 +84,26 @@ cfg // full openclaw config (ctx.cfg from startAccount)
|
|
|
68
84
|
if (!client)
|
|
69
85
|
return;
|
|
70
86
|
const text = typeof payload.text === 'string' ? payload.text.trim() : '';
|
|
71
|
-
if (
|
|
72
|
-
|
|
87
|
+
if ((event === 'entity_message' || event === 'broadcast') && fromEntityId !== undefined) {
|
|
88
|
+
// Bot-to-bot or broadcast: reply via speak-to to the sender
|
|
89
|
+
// Do NOT call sendMessage to avoid duplicate chat history entries
|
|
90
|
+
if (text) {
|
|
91
|
+
await client.speakTo(fromEntityId, text, false);
|
|
92
|
+
}
|
|
73
93
|
}
|
|
74
|
-
else
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
94
|
+
else {
|
|
95
|
+
// Normal human message: reply via channel message
|
|
96
|
+
if (text) {
|
|
97
|
+
await client.sendMessage(text, 'IDLE');
|
|
98
|
+
}
|
|
99
|
+
else if (payload.mediaUrl) {
|
|
100
|
+
const rawType = typeof payload.mediaType === 'string' ? payload.mediaType : '';
|
|
101
|
+
const mediaType = rawType === 'image' ? 'photo'
|
|
102
|
+
: rawType === 'audio' ? 'voice'
|
|
103
|
+
: rawType === 'video' ? 'video'
|
|
104
|
+
: 'file';
|
|
105
|
+
await client.sendMessage('', 'IDLE', mediaType, payload.mediaUrl);
|
|
106
|
+
}
|
|
81
107
|
}
|
|
82
108
|
},
|
|
83
109
|
onError: (err) => {
|