@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.
@@ -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
- * 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.
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
- // 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 };
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
- 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
- },
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
- // OpenClaw inbound dispatch pipeline
53
- const ctx = await rt.channel.reply.finalizeInboundContext(inboundCtx);
54
- await rt.channel.reply.dispatchReplyWithBufferedBlockDispatcher(ctx);
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
+ }
@@ -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", "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.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.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
+ }