@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 +58 -0
- package/dist/channel.d.ts +43 -0
- package/dist/channel.js +43 -0
- package/dist/client.d.ts +25 -0
- package/dist/client.js +93 -0
- package/dist/config.d.ts +5 -0
- package/dist/config.js +23 -0
- package/dist/gateway.d.ts +10 -0
- package/dist/gateway.js +106 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +36 -0
- package/dist/outbound.d.ts +7 -0
- package/dist/outbound.js +56 -0
- package/dist/runtime.d.ts +7 -0
- package/dist/runtime.js +20 -0
- package/dist/types.d.ts +62 -0
- package/dist/types.js +1 -0
- package/dist/webhook-handler.d.ts +7 -0
- package/dist/webhook-handler.js +60 -0
- package/openclaw.plugin.json +27 -0
- package/package.json +60 -0
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
|
+
};
|
package/dist/channel.js
ADDED
|
@@ -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
|
+
};
|
package/dist/client.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -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>;
|
package/dist/gateway.js
ADDED
|
@@ -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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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>;
|
package/dist/outbound.js
ADDED
|
@@ -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;
|
package/dist/runtime.js
ADDED
|
@@ -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
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -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
|
+
}
|