@eclaw/openclaw-channel 1.1.0 → 1.1.2
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 +244 -58
- package/dist/channel.d.ts +19 -0
- package/dist/channel.js +4 -2
- package/dist/client.d.ts +10 -5
- package/dist/client.js +57 -14
- package/dist/config.js +16 -4
- package/dist/gateway.d.ts +5 -6
- package/dist/gateway.js +89 -165
- package/dist/index.d.ts +0 -21
- package/dist/index.js +35 -6
- package/dist/onboarding.d.ts +19 -0
- package/dist/onboarding.js +77 -0
- package/dist/outbound.d.ts +2 -0
- package/dist/outbound.js +18 -0
- package/dist/types.d.ts +19 -2
- package/dist/webhook-handler.d.ts +14 -3
- package/dist/webhook-handler.js +111 -29
- package/dist/webhook-registry.d.ts +19 -0
- package/dist/webhook-registry.js +39 -0
- package/openclaw.plugin.json +27 -27
- package/package.json +60 -60
package/dist/webhook-handler.js
CHANGED
|
@@ -1,11 +1,26 @@
|
|
|
1
1
|
import { getPluginRuntime } from './runtime.js';
|
|
2
|
+
import { getClient, setActiveEvent, clearActiveEvent } from './outbound.js';
|
|
2
3
|
/**
|
|
3
4
|
* Create an HTTP request handler for inbound messages from E-Claw.
|
|
4
5
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
6
|
+
* Handles three event types:
|
|
7
|
+
* - 'message' → Normal human message; reply via sendMessage()
|
|
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]")
|
|
7
19
|
*/
|
|
8
|
-
export function createWebhookHandler(expectedToken, accountId
|
|
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
|
+
) {
|
|
9
24
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
10
25
|
return async (req, res) => {
|
|
11
26
|
// Verify callback token
|
|
@@ -22,36 +37,103 @@ export function createWebhookHandler(expectedToken, accountId) {
|
|
|
22
37
|
// Dispatch to OpenClaw agent
|
|
23
38
|
try {
|
|
24
39
|
const rt = getPluginRuntime();
|
|
40
|
+
const client = getClient(accountId);
|
|
25
41
|
const conversationId = msg.conversationId || `${msg.deviceId}:${msg.entityId}`;
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
+
// Map E-Claw media type to OpenClaw media type
|
|
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');
|
|
34
71
|
}
|
|
72
|
+
// Build context in OpenClaw's native PascalCase format
|
|
73
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
35
74
|
const inboundCtx = {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
75
|
+
Surface: 'eclaw',
|
|
76
|
+
Provider: 'eclaw',
|
|
77
|
+
OriginatingChannel: 'eclaw',
|
|
78
|
+
AccountId: accountId,
|
|
79
|
+
From: msg.from,
|
|
80
|
+
To: conversationId,
|
|
81
|
+
OriginatingTo: msg.from,
|
|
82
|
+
SessionKey: conversationId,
|
|
83
|
+
Body: body,
|
|
84
|
+
RawBody: body,
|
|
85
|
+
CommandBody: body,
|
|
86
|
+
ChatType: 'direct',
|
|
87
|
+
...(ocMediaType && msg.mediaUrl ? {
|
|
88
|
+
MediaType: ocMediaType,
|
|
89
|
+
MediaUrl: msg.mediaUrl,
|
|
90
|
+
} : {}),
|
|
51
91
|
};
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
92
|
+
const ctxPayload = rt.channel.reply.finalizeInboundContext(inboundCtx);
|
|
93
|
+
// Track event type so outbound.sendText() can suppress duplicate delivery
|
|
94
|
+
setActiveEvent(accountId, event);
|
|
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
|
+
}
|
|
55
137
|
}
|
|
56
138
|
catch (err) {
|
|
57
139
|
console.error('[E-Claw] Webhook dispatch error:', err);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-session webhook token registry.
|
|
3
|
+
*
|
|
4
|
+
* Each account generates a random callbackToken when it starts.
|
|
5
|
+
* The token is sent to E-Claw as part of the callback URL registration,
|
|
6
|
+
* and E-Claw echoes it back as `Authorization: Bearer <token>` on every push.
|
|
7
|
+
*
|
|
8
|
+
* The main route handler (registered on the gateway HTTP server) looks up
|
|
9
|
+
* the correct per-account handler by matching the Bearer token.
|
|
10
|
+
*/
|
|
11
|
+
type WebhookHandler = (req: any, res: any) => Promise<void>;
|
|
12
|
+
export declare function registerWebhookToken(callbackToken: string, accountId: string, handler: WebhookHandler): void;
|
|
13
|
+
export declare function unregisterWebhookToken(callbackToken: string): void;
|
|
14
|
+
/**
|
|
15
|
+
* Dispatch an incoming webhook request to the correct account handler.
|
|
16
|
+
* Verifies the Bearer token and routes to the matching handler.
|
|
17
|
+
*/
|
|
18
|
+
export declare function dispatchWebhook(req: any, res: any): Promise<void>;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-session webhook token registry.
|
|
3
|
+
*
|
|
4
|
+
* Each account generates a random callbackToken when it starts.
|
|
5
|
+
* The token is sent to E-Claw as part of the callback URL registration,
|
|
6
|
+
* and E-Claw echoes it back as `Authorization: Bearer <token>` on every push.
|
|
7
|
+
*
|
|
8
|
+
* The main route handler (registered on the gateway HTTP server) looks up
|
|
9
|
+
* the correct per-account handler by matching the Bearer token.
|
|
10
|
+
*/
|
|
11
|
+
const registry = new Map();
|
|
12
|
+
export function registerWebhookToken(callbackToken, accountId, handler) {
|
|
13
|
+
registry.set(callbackToken, { accountId, handler });
|
|
14
|
+
}
|
|
15
|
+
export function unregisterWebhookToken(callbackToken) {
|
|
16
|
+
registry.delete(callbackToken);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Dispatch an incoming webhook request to the correct account handler.
|
|
20
|
+
* Verifies the Bearer token and routes to the matching handler.
|
|
21
|
+
*/
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
23
|
+
export async function dispatchWebhook(req, res) {
|
|
24
|
+
const authHeader = req.headers?.authorization;
|
|
25
|
+
if (!authHeader?.startsWith('Bearer ')) {
|
|
26
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
27
|
+
res.end(JSON.stringify({ error: 'Unauthorized' }));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const token = authHeader.slice(7);
|
|
31
|
+
const entry = registry.get(token);
|
|
32
|
+
if (!entry) {
|
|
33
|
+
// Unknown token — likely a stale push after a server restart
|
|
34
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
35
|
+
res.end(JSON.stringify({ error: 'Unknown token' }));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
await entry.handler(req, res);
|
|
39
|
+
}
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "
|
|
3
|
-
"name": "E-Claw",
|
|
4
|
-
"version": "1.0.0",
|
|
5
|
-
"description": "E-Claw AI
|
|
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", "
|
|
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", "minimum": 0, "maximum": 7, "description": "Optional: entity slot to use (0-7). If omitted, auto-assigned to first free slot." },
|
|
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.
|
|
4
|
-
"description": "E-Claw channel plugin for OpenClaw — AI
|
|
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
|
|
34
|
-
"docsPath": "https://github.com/HankHuang0516/openclaw-channel-eclaw#readme",
|
|
35
|
-
"description": "Connect OpenClaw to E-Claw —
|
|
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": "
|
|
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.2",
|
|
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
|
+
}
|