@eclaw/openclaw-channel 1.0.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 ADDED
@@ -0,0 +1,58 @@
1
+ # @eclaw/openclaw-channel
2
+
3
+ OpenClaw channel plugin for [E-Claw](https://eclawbot.com) — an AI chat platform for live wallpaper entities on Android.
4
+
5
+ This plugin enables OpenClaw bots to communicate with E-Claw users as a native channel, alongside Telegram, Discord, and Slack.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @eclaw/openclaw-channel
11
+ ```
12
+
13
+ ## Configuration
14
+
15
+ Add to your OpenClaw `config.yaml`:
16
+
17
+ ```yaml
18
+ plugins:
19
+ - "@eclaw/openclaw-channel"
20
+
21
+ channels:
22
+ eclaw:
23
+ accounts:
24
+ default:
25
+ apiKey: "eck_..." # From E-Claw Portal → Settings → Channel API
26
+ apiSecret: "ecs_..." # From E-Claw Portal → Settings → Channel API
27
+ apiBase: "https://eclawbot.com"
28
+ entityId: 0 # Which entity slot to use (0-3)
29
+ botName: "My Bot"
30
+ ```
31
+
32
+ ## Getting API Credentials
33
+
34
+ 1. Log in to [E-Claw Portal](https://eclawbot.com/portal)
35
+ 2. Go to **Settings → Channel API**
36
+ 3. Copy your `API Key` and `API Secret`
37
+
38
+ ## How It Works
39
+
40
+ ```
41
+ User (Android) ──speaks──▶ E-Claw Backend ──webhook──▶ OpenClaw Agent
42
+ OpenClaw Agent ──replies──▶ POST /api/channel/message ──▶ User (Android)
43
+ ```
44
+
45
+ - **Inbound**: E-Claw POSTs structured JSON to a webhook URL registered by this plugin
46
+ - **Outbound**: Plugin calls `POST /api/channel/message` with the bot reply
47
+ - **Auth**: `eck_`/`ecs_` channel credentials for API auth, per-entity `botSecret` for message auth
48
+
49
+ ## Environment Variables
50
+
51
+ | Variable | Required | Description |
52
+ |----------|----------|-------------|
53
+ | `ECLAW_WEBHOOK_URL` | Production | Public URL for receiving inbound messages |
54
+ | `ECLAW_WEBHOOK_PORT` | Optional | Webhook server port (default: random) |
55
+
56
+ ## License
57
+
58
+ MIT
@@ -0,0 +1,43 @@
1
+ import { listAccountIds, resolveAccount } from './config.js';
2
+ import { sendText, sendMedia } from './outbound.js';
3
+ import { startAccount } from './gateway.js';
4
+ /**
5
+ * E-Claw ChannelPlugin definition.
6
+ *
7
+ * This is the core contract that OpenClaw requires for any channel provider.
8
+ * It enables E-Claw to appear alongside Telegram, Discord, Slack, etc.
9
+ * in the OpenClaw channel list.
10
+ */
11
+ export declare const eclawChannel: {
12
+ id: string;
13
+ meta: {
14
+ id: string;
15
+ label: string;
16
+ selectionLabel: string;
17
+ docsPath: string;
18
+ blurb: string;
19
+ aliases: string[];
20
+ };
21
+ capabilities: {
22
+ chatTypes: readonly ["direct"];
23
+ media: boolean;
24
+ reactions: boolean;
25
+ threads: boolean;
26
+ polls: boolean;
27
+ nativeCommands: boolean;
28
+ blockStreaming: boolean;
29
+ };
30
+ config: {
31
+ listAccountIds: typeof listAccountIds;
32
+ resolveAccount: typeof resolveAccount;
33
+ };
34
+ outbound: {
35
+ deliveryMode: "direct";
36
+ textChunkLimit: number;
37
+ sendText: typeof sendText;
38
+ sendMedia: typeof sendMedia;
39
+ };
40
+ gateway: {
41
+ startAccount: typeof startAccount;
42
+ };
43
+ };
@@ -0,0 +1,43 @@
1
+ import { listAccountIds, resolveAccount } from './config.js';
2
+ import { sendText, sendMedia } from './outbound.js';
3
+ import { startAccount } from './gateway.js';
4
+ /**
5
+ * E-Claw ChannelPlugin definition.
6
+ *
7
+ * This is the core contract that OpenClaw requires for any channel provider.
8
+ * It enables E-Claw to appear alongside Telegram, Discord, Slack, etc.
9
+ * in the OpenClaw channel list.
10
+ */
11
+ export const eclawChannel = {
12
+ id: 'eclaw',
13
+ meta: {
14
+ id: 'eclaw',
15
+ label: 'E-Claw',
16
+ selectionLabel: 'E-Claw (AI Live Wallpaper Chat)',
17
+ docsPath: '/channels/eclaw',
18
+ blurb: 'Connect OpenClaw to E-Claw — an AI chat platform for live wallpaper entities on Android.',
19
+ aliases: ['eclaw', 'claw', 'e-claw'],
20
+ },
21
+ capabilities: {
22
+ chatTypes: ['direct'],
23
+ media: true,
24
+ reactions: false,
25
+ threads: false,
26
+ polls: false,
27
+ nativeCommands: false,
28
+ blockStreaming: false,
29
+ },
30
+ config: {
31
+ listAccountIds,
32
+ resolveAccount,
33
+ },
34
+ outbound: {
35
+ deliveryMode: 'direct',
36
+ textChunkLimit: 4000,
37
+ sendText,
38
+ sendMedia,
39
+ },
40
+ gateway: {
41
+ startAccount,
42
+ },
43
+ };
@@ -0,0 +1,25 @@
1
+ import type { EClawAccountConfig, RegisterResponse, BindResponse, MessageResponse } from './types.js';
2
+ /**
3
+ * HTTP client for E-Claw Channel API.
4
+ * Handles all communication between the OpenClaw plugin and the E-Claw backend.
5
+ */
6
+ export declare class EClawClient {
7
+ private readonly apiBase;
8
+ private readonly apiKey;
9
+ private readonly apiSecret;
10
+ private deviceId;
11
+ private botSecret;
12
+ private entityId;
13
+ constructor(config: EClawAccountConfig);
14
+ /** Register callback URL with E-Claw backend */
15
+ registerCallback(callbackUrl: string, callbackToken: string): Promise<RegisterResponse>;
16
+ /** Bind an entity via channel API (bypasses 6-digit code) */
17
+ bindEntity(entityId: number, name?: string): Promise<BindResponse>;
18
+ /** Send bot message to user */
19
+ sendMessage(message: string, state?: string, mediaType?: string, mediaUrl?: string): Promise<MessageResponse>;
20
+ /** Unregister callback on shutdown */
21
+ unregisterCallback(): Promise<void>;
22
+ get currentDeviceId(): string | null;
23
+ get currentBotSecret(): string | null;
24
+ get currentEntityId(): number;
25
+ }
package/dist/client.js ADDED
@@ -0,0 +1,93 @@
1
+ /**
2
+ * HTTP client for E-Claw Channel API.
3
+ * Handles all communication between the OpenClaw plugin and the E-Claw backend.
4
+ */
5
+ export class EClawClient {
6
+ apiBase;
7
+ apiKey;
8
+ apiSecret;
9
+ deviceId = null;
10
+ botSecret = null;
11
+ entityId;
12
+ constructor(config) {
13
+ this.apiBase = config.apiBase;
14
+ this.apiKey = config.apiKey;
15
+ this.apiSecret = config.apiSecret;
16
+ this.entityId = config.entityId;
17
+ }
18
+ /** Register callback URL with E-Claw backend */
19
+ async registerCallback(callbackUrl, callbackToken) {
20
+ const res = await fetch(`${this.apiBase}/api/channel/register`, {
21
+ method: 'POST',
22
+ headers: { 'Content-Type': 'application/json' },
23
+ body: JSON.stringify({
24
+ channel_api_key: this.apiKey,
25
+ channel_api_secret: this.apiSecret,
26
+ callback_url: callbackUrl,
27
+ callback_token: callbackToken,
28
+ }),
29
+ });
30
+ const data = await res.json();
31
+ if (!data.success) {
32
+ throw new Error(data.message || `Registration failed (HTTP ${res.status})`);
33
+ }
34
+ this.deviceId = data.deviceId;
35
+ return data;
36
+ }
37
+ /** Bind an entity via channel API (bypasses 6-digit code) */
38
+ async bindEntity(entityId, name) {
39
+ const res = await fetch(`${this.apiBase}/api/channel/bind`, {
40
+ method: 'POST',
41
+ headers: { 'Content-Type': 'application/json' },
42
+ body: JSON.stringify({
43
+ channel_api_key: this.apiKey,
44
+ channel_api_secret: this.apiSecret,
45
+ entityId,
46
+ name: name || undefined,
47
+ }),
48
+ });
49
+ const data = await res.json();
50
+ if (!data.success) {
51
+ throw new Error(data.message || `Bind failed (HTTP ${res.status})`);
52
+ }
53
+ this.botSecret = data.botSecret;
54
+ this.deviceId = data.deviceId;
55
+ this.entityId = entityId;
56
+ return data;
57
+ }
58
+ /** Send bot message to user */
59
+ async sendMessage(message, state = 'IDLE', mediaType, mediaUrl) {
60
+ if (!this.deviceId || !this.botSecret) {
61
+ throw new Error('Not bound — call bindEntity() first');
62
+ }
63
+ const res = await fetch(`${this.apiBase}/api/channel/message`, {
64
+ method: 'POST',
65
+ headers: { 'Content-Type': 'application/json' },
66
+ body: JSON.stringify({
67
+ channel_api_key: this.apiKey,
68
+ deviceId: this.deviceId,
69
+ entityId: this.entityId,
70
+ botSecret: this.botSecret,
71
+ message,
72
+ state,
73
+ ...(mediaType && { mediaType }),
74
+ ...(mediaUrl && { mediaUrl }),
75
+ }),
76
+ });
77
+ return await res.json();
78
+ }
79
+ /** Unregister callback on shutdown */
80
+ async unregisterCallback() {
81
+ await fetch(`${this.apiBase}/api/channel/register`, {
82
+ method: 'DELETE',
83
+ headers: { 'Content-Type': 'application/json' },
84
+ body: JSON.stringify({
85
+ channel_api_key: this.apiKey,
86
+ channel_api_secret: this.apiSecret,
87
+ }),
88
+ });
89
+ }
90
+ get currentDeviceId() { return this.deviceId; }
91
+ get currentBotSecret() { return this.botSecret; }
92
+ get currentEntityId() { return this.entityId; }
93
+ }
@@ -0,0 +1,5 @@
1
+ import type { EClawAccountConfig } from './types.js';
2
+ /** List all configured account IDs from OpenClaw config */
3
+ export declare function listAccountIds(cfg: any): string[];
4
+ /** Resolve a specific account's config, with defaults */
5
+ export declare function resolveAccount(cfg: any, accountId?: string): EClawAccountConfig;
package/dist/config.js ADDED
@@ -0,0 +1,23 @@
1
+ /** List all configured account IDs from OpenClaw config */
2
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
3
+ export function listAccountIds(cfg) {
4
+ const accounts = cfg?.channels?.eclaw?.accounts;
5
+ if (!accounts || typeof accounts !== 'object')
6
+ return [];
7
+ return Object.keys(accounts);
8
+ }
9
+ /** Resolve a specific account's config, with defaults */
10
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
+ export function resolveAccount(cfg, accountId) {
12
+ const accounts = cfg?.channels?.eclaw?.accounts ?? {};
13
+ const id = accountId ?? Object.keys(accounts)[0] ?? 'default';
14
+ const account = accounts[id];
15
+ return {
16
+ enabled: account?.enabled ?? true,
17
+ apiKey: account?.apiKey ?? '',
18
+ apiSecret: account?.apiSecret ?? '',
19
+ apiBase: (account?.apiBase ?? 'https://eclawbot.com').replace(/\/$/, ''),
20
+ entityId: account?.entityId ?? 0,
21
+ botName: account?.botName,
22
+ };
23
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Gateway lifecycle: start an E-Claw account.
3
+ *
4
+ * 1. Initialize HTTP client with channel API credentials
5
+ * 2. Start a local HTTP server to receive webhook callbacks
6
+ * 3. Register callback URL with E-Claw backend
7
+ * 4. Auto-bind entity if not already bound
8
+ * 5. Keep the promise alive until abort signal fires
9
+ */
10
+ export declare function startAccount(ctx: any): Promise<void>;
@@ -0,0 +1,106 @@
1
+ import { createServer } from 'node:http';
2
+ import { randomBytes } from 'node:crypto';
3
+ import { resolveAccount } from './config.js';
4
+ import { EClawClient } from './client.js';
5
+ import { setClient } from './outbound.js';
6
+ import { createWebhookHandler } from './webhook-handler.js';
7
+ /**
8
+ * Gateway lifecycle: start an E-Claw account.
9
+ *
10
+ * 1. Initialize HTTP client with channel API credentials
11
+ * 2. Start a local HTTP server to receive webhook callbacks
12
+ * 3. Register callback URL with E-Claw backend
13
+ * 4. Auto-bind entity if not already bound
14
+ * 5. Keep the promise alive until abort signal fires
15
+ */
16
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
17
+ export async function startAccount(ctx) {
18
+ const { accountId, config } = ctx;
19
+ const account = resolveAccount(config, accountId);
20
+ if (!account.enabled || !account.apiKey || !account.apiSecret) {
21
+ console.log(`[E-Claw] Account ${accountId} disabled or missing credentials, skipping`);
22
+ return;
23
+ }
24
+ // Initialize HTTP client
25
+ const client = new EClawClient(account);
26
+ setClient(accountId, client);
27
+ // Generate per-session callback token
28
+ const callbackToken = randomBytes(32).toString('hex');
29
+ // Determine webhook configuration
30
+ const webhookPort = parseInt(process.env.ECLAW_WEBHOOK_PORT || '0') || 0;
31
+ const publicUrl = process.env.ECLAW_WEBHOOK_URL;
32
+ if (!publicUrl) {
33
+ console.warn('[E-Claw] ECLAW_WEBHOOK_URL not set. Set this to your public-facing URL ' +
34
+ 'so E-Claw can send messages to this plugin. Example: https://my-openclaw.example.com');
35
+ }
36
+ // Create webhook handler
37
+ const handler = createWebhookHandler(callbackToken, accountId);
38
+ // Parse JSON body for incoming requests
39
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
40
+ const requestHandler = (req, res) => {
41
+ if (req.method === 'POST' && req.url?.startsWith('/eclaw-webhook')) {
42
+ let body = '';
43
+ req.on('data', (chunk) => { body += chunk.toString(); });
44
+ req.on('end', () => {
45
+ try {
46
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
47
+ req.body = JSON.parse(body);
48
+ }
49
+ catch {
50
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
51
+ req.body = {};
52
+ }
53
+ handler(req, res);
54
+ });
55
+ }
56
+ else {
57
+ res.writeHead(404);
58
+ res.end('Not Found');
59
+ }
60
+ };
61
+ const server = createServer(requestHandler);
62
+ return new Promise((resolve) => {
63
+ server.listen(webhookPort, async () => {
64
+ const addr = server.address();
65
+ const actualPort = typeof addr === 'object' && addr ? addr.port : webhookPort;
66
+ const baseUrl = publicUrl || `http://localhost:${actualPort}`;
67
+ const callbackUrl = `${baseUrl}/eclaw-webhook`;
68
+ console.log(`[E-Claw] Webhook server listening on port ${actualPort}`);
69
+ console.log(`[E-Claw] Callback URL: ${callbackUrl}`);
70
+ try {
71
+ // Register callback with E-Claw backend
72
+ const regData = await client.registerCallback(callbackUrl, callbackToken);
73
+ console.log(`[E-Claw] Registered with E-Claw. Device: ${regData.deviceId}, Entities: ${regData.entities.length}`);
74
+ // Auto-bind entity if not already bound
75
+ const entity = regData.entities.find(e => e.entityId === account.entityId);
76
+ if (!entity?.isBound) {
77
+ console.log(`[E-Claw] Entity ${account.entityId} not bound, binding...`);
78
+ const bindData = await client.bindEntity(account.entityId, account.botName);
79
+ console.log(`[E-Claw] Bound entity ${account.entityId}, publicCode: ${bindData.publicCode}`);
80
+ }
81
+ else {
82
+ console.log(`[E-Claw] Entity ${account.entityId} already bound`);
83
+ // For already-bound entities, we need to get the botSecret
84
+ // The bind endpoint returns existing credentials for channel-bound entities
85
+ const bindData = await client.bindEntity(account.entityId, account.botName);
86
+ console.log(`[E-Claw] Retrieved credentials for entity ${account.entityId}`);
87
+ void bindData; // credentials stored in client
88
+ }
89
+ console.log(`[E-Claw] Account ${accountId} ready!`);
90
+ }
91
+ catch (err) {
92
+ console.error(`[E-Claw] Setup failed for account ${accountId}:`, err);
93
+ }
94
+ });
95
+ // Cleanup on abort
96
+ const signal = ctx.abortSignal;
97
+ if (signal) {
98
+ signal.addEventListener('abort', () => {
99
+ console.log(`[E-Claw] Shutting down account ${accountId}`);
100
+ client.unregisterCallback().catch(() => { });
101
+ server.close();
102
+ resolve();
103
+ });
104
+ }
105
+ });
106
+ }
@@ -0,0 +1,28 @@
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
+ */
22
+ declare const plugin: {
23
+ id: string;
24
+ name: string;
25
+ description: string;
26
+ register(api: any): void;
27
+ };
28
+ export default plugin;
package/dist/index.js ADDED
@@ -0,0 +1,36 @@
1
+ import { setPluginRuntime } from './runtime.js';
2
+ import { eclawChannel } from './channel.js';
3
+ /**
4
+ * E-Claw Channel Plugin for OpenClaw.
5
+ *
6
+ * Installation:
7
+ * npm install @eclaw/openclaw-channel
8
+ *
9
+ * Configuration (config.yaml):
10
+ * channels:
11
+ * eclaw:
12
+ * accounts:
13
+ * default:
14
+ * apiKey: "eck_..."
15
+ * apiSecret: "ecs_..."
16
+ * apiBase: "https://eclawbot.com"
17
+ * entityId: 0
18
+ * botName: "My Bot"
19
+ *
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)
23
+ */
24
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
25
+ const plugin = {
26
+ id: 'eclaw',
27
+ name: 'E-Claw',
28
+ description: 'E-Claw AI chat platform channel plugin',
29
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
30
+ register(api) {
31
+ console.log('[E-Claw] Plugin loaded');
32
+ setPluginRuntime(api.runtime);
33
+ api.registerChannel({ plugin: eclawChannel });
34
+ },
35
+ };
36
+ export default plugin;
@@ -0,0 +1,7 @@
1
+ import { EClawClient } from './client.js';
2
+ export declare function setClient(accountId: string, client: EClawClient): void;
3
+ export declare function getClient(accountId: string): EClawClient | undefined;
4
+ /** OpenClaw outbound: send text message to E-Claw user */
5
+ export declare function sendText(ctx: any): Promise<any>;
6
+ /** OpenClaw outbound: send media message to E-Claw user */
7
+ export declare function sendMedia(ctx: any): Promise<any>;
@@ -0,0 +1,56 @@
1
+ /** Client instances keyed by accountId */
2
+ const clients = new Map();
3
+ export function setClient(accountId, client) {
4
+ clients.set(accountId, client);
5
+ }
6
+ export function getClient(accountId) {
7
+ return clients.get(accountId);
8
+ }
9
+ /** OpenClaw outbound: send text message to E-Claw user */
10
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
+ export async function sendText(ctx) {
12
+ const accountId = ctx.accountId ?? 'default';
13
+ const client = clients.get(accountId);
14
+ if (!client) {
15
+ return { channel: 'eclaw', messageId: '', chatId: '' };
16
+ }
17
+ try {
18
+ const result = await client.sendMessage(ctx.text, 'IDLE');
19
+ return {
20
+ channel: 'eclaw',
21
+ messageId: `eclaw-${Date.now()}`,
22
+ chatId: ctx.to ?? ctx.conversationId ?? '',
23
+ ok: result.success,
24
+ };
25
+ }
26
+ catch (err) {
27
+ console.error('[E-Claw] sendText failed:', err);
28
+ return { channel: 'eclaw', messageId: '', chatId: '' };
29
+ }
30
+ }
31
+ /** OpenClaw outbound: send media message to E-Claw user */
32
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
33
+ export async function sendMedia(ctx) {
34
+ const accountId = ctx.accountId ?? 'default';
35
+ const client = clients.get(accountId);
36
+ if (!client) {
37
+ return { channel: 'eclaw', messageId: '', chatId: '' };
38
+ }
39
+ try {
40
+ // Map OpenClaw media types to E-Claw types
41
+ const mediaType = ctx.mediaType === 'image' ? 'photo'
42
+ : ctx.mediaType === 'audio' ? 'voice'
43
+ : ctx.mediaType ?? 'file';
44
+ const result = await client.sendMessage(ctx.text || `[${mediaType}]`, 'IDLE', mediaType, ctx.mediaUrl);
45
+ return {
46
+ channel: 'eclaw',
47
+ messageId: `eclaw-${Date.now()}`,
48
+ chatId: ctx.to ?? ctx.conversationId ?? '',
49
+ ok: result.success,
50
+ };
51
+ }
52
+ catch (err) {
53
+ console.error('[E-Claw] sendMedia failed:', err);
54
+ return { channel: 'eclaw', messageId: '', chatId: '' };
55
+ }
56
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * PluginRuntime singleton storage.
3
+ * Every OpenClaw channel plugin follows this pattern — store the runtime
4
+ * received during register() so other modules can access it.
5
+ */
6
+ export declare function setPluginRuntime(runtime: any): void;
7
+ export declare function getPluginRuntime(): any;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * PluginRuntime singleton storage.
3
+ * Every OpenClaw channel plugin follows this pattern — store the runtime
4
+ * received during register() so other modules can access it.
5
+ */
6
+ // Using `any` because the PluginRuntime type comes from openclaw/plugin-sdk
7
+ // which is only available when installed as a plugin within OpenClaw.
8
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9
+ let pluginRuntime = null;
10
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
+ export function setPluginRuntime(runtime) {
12
+ pluginRuntime = runtime;
13
+ }
14
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
15
+ export function getPluginRuntime() {
16
+ if (!pluginRuntime) {
17
+ throw new Error('[E-Claw] Plugin runtime not initialized');
18
+ }
19
+ return pluginRuntime;
20
+ }
@@ -0,0 +1,62 @@
1
+ /** E-Claw account configuration from OpenClaw config.yaml */
2
+ export interface EClawAccountConfig {
3
+ enabled: boolean;
4
+ apiKey: string;
5
+ apiSecret: string;
6
+ apiBase: string;
7
+ entityId: number;
8
+ botName?: string;
9
+ }
10
+ /** Inbound message from E-Claw callback webhook */
11
+ export interface EClawInboundMessage {
12
+ event: 'message' | 'entity_message' | 'broadcast' | 'cross_device_message';
13
+ deviceId: string;
14
+ entityId: number;
15
+ conversationId: string;
16
+ from: string;
17
+ text: string;
18
+ mediaType?: 'photo' | 'voice' | 'video' | 'file' | null;
19
+ mediaUrl?: string | null;
20
+ backupUrl?: string | null;
21
+ timestamp: number;
22
+ isBroadcast: boolean;
23
+ broadcastRecipients?: number[] | null;
24
+ fromEntityId?: number;
25
+ fromCharacter?: string;
26
+ fromPublicCode?: string;
27
+ }
28
+ /** Entity info returned by channel register */
29
+ export interface EClawEntityInfo {
30
+ entityId: number;
31
+ isBound: boolean;
32
+ name: string | null;
33
+ character: string;
34
+ bindingType: string | null;
35
+ }
36
+ /** Response from POST /api/channel/register */
37
+ export interface RegisterResponse {
38
+ success: boolean;
39
+ deviceId: string;
40
+ entities: EClawEntityInfo[];
41
+ maxEntities: number;
42
+ }
43
+ /** Response from POST /api/channel/bind */
44
+ export interface BindResponse {
45
+ success: boolean;
46
+ deviceId: string;
47
+ entityId: number;
48
+ botSecret: string;
49
+ publicCode: string;
50
+ bindingType: string;
51
+ }
52
+ /** Response from POST /api/channel/message */
53
+ export interface MessageResponse {
54
+ success: boolean;
55
+ currentState?: {
56
+ name: string;
57
+ state: string;
58
+ message: string;
59
+ xp: number;
60
+ level: number;
61
+ };
62
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Create an HTTP request handler for inbound messages from E-Claw.
3
+ *
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.
6
+ */
7
+ export declare function createWebhookHandler(expectedToken: string, accountId: string): (req: any, res: any) => Promise<void>;
@@ -0,0 +1,60 @@
1
+ import { getPluginRuntime } from './runtime.js';
2
+ /**
3
+ * Create an HTTP request handler for inbound messages from E-Claw.
4
+ *
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.
7
+ */
8
+ export function createWebhookHandler(expectedToken, accountId) {
9
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
10
+ return async (req, res) => {
11
+ // Verify callback token
12
+ const authHeader = req.headers?.authorization;
13
+ if (expectedToken && (!authHeader || authHeader !== `Bearer ${expectedToken}`)) {
14
+ res.writeHead(401, { 'Content-Type': 'application/json' });
15
+ res.end(JSON.stringify({ error: 'Unauthorized' }));
16
+ return;
17
+ }
18
+ const msg = req.body;
19
+ // ACK immediately so E-Claw doesn't time out
20
+ res.writeHead(200, { 'Content-Type': 'application/json' });
21
+ res.end(JSON.stringify({ ok: true }));
22
+ // Dispatch to OpenClaw agent
23
+ try {
24
+ const rt = getPluginRuntime();
25
+ 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 };
34
+ }
35
+ 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
+ },
51
+ };
52
+ // OpenClaw inbound dispatch pipeline
53
+ const ctx = await rt.channel.reply.finalizeInboundContext(inboundCtx);
54
+ await rt.channel.reply.dispatchReplyWithBufferedBlockDispatcher(ctx);
55
+ }
56
+ catch (err) {
57
+ console.error('[E-Claw] Webhook dispatch error:', err);
58
+ }
59
+ };
60
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "id": "eclaw",
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", "default": 0, "minimum": 0, "maximum": 7 },
20
+ "botName": { "type": "string", "maxLength": 20 }
21
+ },
22
+ "required": ["apiKey", "apiSecret"]
23
+ }
24
+ }
25
+ }
26
+ }
27
+ }
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@eclaw/openclaw-channel",
3
+ "version": "1.0.0",
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": "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
+ }