@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/gateway.js
CHANGED
|
@@ -1,51 +1,57 @@
|
|
|
1
|
-
import { createServer } from 'node:http';
|
|
2
1
|
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
3
5
|
import { resolveAccount } from './config.js';
|
|
4
6
|
import { EClawClient } from './client.js';
|
|
5
7
|
import { setClient } from './outbound.js';
|
|
6
8
|
import { createWebhookHandler } from './webhook-handler.js';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
9
|
+
import { registerWebhookToken, unregisterWebhookToken } from './webhook-registry.js';
|
|
10
|
+
/**
|
|
11
|
+
* Resolve account from ctx.
|
|
12
|
+
*
|
|
13
|
+
* OpenClaw may pass a pre-resolved account object in ctx.account,
|
|
14
|
+
* or an empty config. Fall back to reading openclaw.json from disk.
|
|
15
|
+
*/
|
|
16
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
17
|
+
function resolveAccountFromCtx(ctx) {
|
|
18
|
+
// Preferred: OpenClaw passes the resolved account in ctx.account
|
|
19
|
+
if (ctx.account?.apiKey) {
|
|
20
|
+
return {
|
|
21
|
+
enabled: ctx.account.enabled ?? true,
|
|
22
|
+
apiKey: ctx.account.apiKey,
|
|
23
|
+
apiSecret: ctx.account.apiSecret,
|
|
24
|
+
apiBase: (ctx.account.apiBase ?? 'https://eclawbot.com').replace(/\/$/, ''),
|
|
25
|
+
entityId: ctx.account.entityId, // undefined = auto-select
|
|
26
|
+
botName: ctx.account.botName,
|
|
27
|
+
webhookUrl: ctx.account.webhookUrl,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
// Fallback: read config from disk (OpenClaw passes empty config object)
|
|
31
|
+
const configPath = process.env.OPENCLAW_CONFIG_PATH
|
|
32
|
+
|| join(homedir(), '.openclaw', 'openclaw.json');
|
|
33
|
+
let fullConfig = {};
|
|
34
|
+
try {
|
|
35
|
+
fullConfig = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
36
|
+
}
|
|
37
|
+
catch { /* ignore */ }
|
|
38
|
+
return resolveAccount(fullConfig, ctx.accountId ?? ctx.account?.accountId);
|
|
32
39
|
}
|
|
33
40
|
/**
|
|
34
41
|
* Gateway lifecycle: start an E-Claw account.
|
|
35
42
|
*
|
|
36
|
-
* 1.
|
|
37
|
-
* 2.
|
|
38
|
-
*
|
|
43
|
+
* 1. Resolve credentials from ctx.account or disk
|
|
44
|
+
* 2. Register a per-session handler in the webhook-registry (served by the
|
|
45
|
+
* main OpenClaw gateway HTTP server at /eclaw-webhook — no separate port)
|
|
46
|
+
* 3. Register callback URL with E-Claw backend
|
|
39
47
|
* 4. Auto-bind entity if not already bound
|
|
40
|
-
* 5.
|
|
41
|
-
* 6. On health-check failure, reconnect with exponential backoff
|
|
42
|
-
* 7. Keep the promise alive until abort signal fires
|
|
48
|
+
* 5. Keep the promise alive until abort signal fires
|
|
43
49
|
*/
|
|
44
50
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
45
51
|
export async function startAccount(ctx) {
|
|
46
|
-
const
|
|
47
|
-
const account =
|
|
48
|
-
if (!account.enabled || !account.apiKey
|
|
52
|
+
const accountId = ctx.accountId ?? ctx.account?.accountId ?? 'default';
|
|
53
|
+
const account = resolveAccountFromCtx(ctx);
|
|
54
|
+
if (!account.enabled || !account.apiKey) {
|
|
49
55
|
console.log(`[E-Claw] Account ${accountId} disabled or missing credentials, skipping`);
|
|
50
56
|
return;
|
|
51
57
|
}
|
|
@@ -54,141 +60,59 @@ export async function startAccount(ctx) {
|
|
|
54
60
|
setClient(accountId, client);
|
|
55
61
|
// Generate per-session callback token
|
|
56
62
|
const callbackToken = randomBytes(32).toString('hex');
|
|
57
|
-
//
|
|
58
|
-
const
|
|
59
|
-
|
|
63
|
+
// Webhook URL: account config > env var > warn
|
|
64
|
+
const publicUrl = account.webhookUrl?.replace(/\/$/, '')
|
|
65
|
+
|| process.env.ECLAW_WEBHOOK_URL?.replace(/\/$/, '');
|
|
60
66
|
if (!publicUrl) {
|
|
61
|
-
console.warn('[E-Claw]
|
|
62
|
-
'
|
|
67
|
+
console.warn('[E-Claw] Webhook URL not configured. ' +
|
|
68
|
+
'Run "openclaw configure" and enter your OpenClaw public URL, ' +
|
|
69
|
+
'or set ECLAW_WEBHOOK_URL env var. ' +
|
|
70
|
+
'Example: https://your-openclaw-domain.com');
|
|
63
71
|
}
|
|
64
|
-
//
|
|
65
|
-
const
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
72
|
+
// The callback URL points to /eclaw-webhook on the main gateway HTTP server
|
|
73
|
+
const callbackUrl = `${publicUrl || 'http://localhost'}/eclaw-webhook`;
|
|
74
|
+
// Register handler in the per-token registry
|
|
75
|
+
// Pass ctx.cfg so the handler can dispatch to the correct OpenClaw agent
|
|
76
|
+
const handler = createWebhookHandler(callbackToken, accountId, ctx.cfg);
|
|
77
|
+
registerWebhookToken(callbackToken, accountId, handler);
|
|
78
|
+
console.log(`[E-Claw] Webhook registered at: ${callbackUrl}`);
|
|
79
|
+
try {
|
|
80
|
+
// Register callback with E-Claw backend
|
|
81
|
+
const regData = await client.registerCallback(callbackUrl, callbackToken);
|
|
82
|
+
console.log(`[E-Claw] Registered with E-Claw. Device: ${regData.deviceId}, Entities: ${regData.entities.length}`);
|
|
83
|
+
// Bind entity via channel API.
|
|
84
|
+
// /api/channel/bind is idempotent for the same channel account:
|
|
85
|
+
// - Not bound → binds fresh, returns new botSecret
|
|
86
|
+
// - Already bound via this channel account → returns existing botSecret (reconnect)
|
|
87
|
+
// - Bound via different method → throws error (user must unbind first)
|
|
88
|
+
// entityId is omitted here so the server auto-selects the best slot
|
|
89
|
+
const bindData = await client.bindEntity(account.entityId, account.botName);
|
|
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}`);
|
|
96
|
+
console.log(`[E-Claw] Account ${accountId} ready!`);
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
console.error(`[E-Claw] Setup failed for account ${accountId}:`, err);
|
|
100
|
+
unregisterWebhookToken(callbackToken);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
// Keep the promise alive until abort signal fires
|
|
104
|
+
return new Promise((resolve) => {
|
|
105
|
+
const signal = ctx.abortSignal;
|
|
106
|
+
if (signal) {
|
|
107
|
+
signal.addEventListener('abort', () => {
|
|
108
|
+
console.log(`[E-Claw] Shutting down account ${accountId}`);
|
|
109
|
+
client.unregisterCallback().catch(() => { });
|
|
110
|
+
unregisterWebhookToken(callbackToken);
|
|
111
|
+
resolve();
|
|
82
112
|
});
|
|
83
113
|
}
|
|
84
114
|
else {
|
|
85
|
-
|
|
86
|
-
res.end('Not Found');
|
|
115
|
+
resolve();
|
|
87
116
|
}
|
|
88
|
-
};
|
|
89
|
-
const server = createServer(requestHandler);
|
|
90
|
-
return new Promise((resolve) => {
|
|
91
|
-
server.listen(webhookPort, async () => {
|
|
92
|
-
const addr = server.address();
|
|
93
|
-
const actualPort = typeof addr === 'object' && addr ? addr.port : webhookPort;
|
|
94
|
-
console.log(`[E-Claw] Webhook server listening on port ${actualPort}`);
|
|
95
|
-
const signal = ctx.abortSignal;
|
|
96
|
-
// ── Core setup: register callback + bind entity ─────────────────────
|
|
97
|
-
/**
|
|
98
|
-
* One full register+bind cycle. Returns true on success, false on failure.
|
|
99
|
-
* Reads ECLAW_WEBHOOK_URL fresh every call so a corrected env var takes
|
|
100
|
-
* effect automatically on the next reconnect.
|
|
101
|
-
*/
|
|
102
|
-
async function attemptSetup() {
|
|
103
|
-
const callbackUrl = buildCallbackUrl(actualPort);
|
|
104
|
-
console.log(`[E-Claw][${accountId}] Registering callback: ${callbackUrl}`);
|
|
105
|
-
try {
|
|
106
|
-
const regData = await client.registerCallback(callbackUrl, callbackToken);
|
|
107
|
-
console.log(`[E-Claw][${accountId}] Registered. Device: ${regData.deviceId}, Entities: ${regData.entities.length}`);
|
|
108
|
-
const entity = regData.entities.find(e => e.entityId === account.entityId);
|
|
109
|
-
if (!entity?.isBound) {
|
|
110
|
-
console.log(`[E-Claw][${accountId}] Entity ${account.entityId} not bound, binding...`);
|
|
111
|
-
const bindData = await client.bindEntity(account.entityId, account.botName);
|
|
112
|
-
console.log(`[E-Claw][${accountId}] Bound entity ${account.entityId}, publicCode: ${bindData.publicCode}`);
|
|
113
|
-
}
|
|
114
|
-
else {
|
|
115
|
-
console.log(`[E-Claw][${accountId}] Entity ${account.entityId} already bound`);
|
|
116
|
-
// For already-bound entities, retrieve credentials via bind endpoint
|
|
117
|
-
const bindData = await client.bindEntity(account.entityId, account.botName);
|
|
118
|
-
console.log(`[E-Claw][${accountId}] Retrieved credentials for entity ${account.entityId}`);
|
|
119
|
-
void bindData; // credentials stored in client
|
|
120
|
-
}
|
|
121
|
-
return true;
|
|
122
|
-
}
|
|
123
|
-
catch (err) {
|
|
124
|
-
console.error(`[E-Claw][${accountId}] Setup attempt failed:`, err);
|
|
125
|
-
return false;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
// ── Initial connect with exponential backoff ────────────────────────
|
|
129
|
-
let backoffMs = BACKOFF_INITIAL_MS;
|
|
130
|
-
let attempt = 0;
|
|
131
|
-
while (!signal?.aborted) {
|
|
132
|
-
attempt++;
|
|
133
|
-
const ok = await attemptSetup();
|
|
134
|
-
if (ok) {
|
|
135
|
-
console.log(`[E-Claw][${accountId}] Account ready! (attempt #${attempt})`);
|
|
136
|
-
break;
|
|
137
|
-
}
|
|
138
|
-
const delay = jitter(Math.min(backoffMs, BACKOFF_MAX_MS));
|
|
139
|
-
console.warn(`[E-Claw][${accountId}] Retrying in ${Math.round(delay / 1000)}s (attempt #${attempt})...`);
|
|
140
|
-
await sleep(delay, signal);
|
|
141
|
-
backoffMs = Math.min(backoffMs * BACKOFF_MULTIPLIER, BACKOFF_MAX_MS);
|
|
142
|
-
}
|
|
143
|
-
if (signal?.aborted)
|
|
144
|
-
return;
|
|
145
|
-
// ── Periodic health check + auto-reconnect ──────────────────────────
|
|
146
|
-
// Re-register every 60 s. If it fails, enter a reconnect backoff loop.
|
|
147
|
-
// Guard flag prevents concurrent reconnect loops from stacking.
|
|
148
|
-
let isReconnecting = false;
|
|
149
|
-
async function runHealthCheck() {
|
|
150
|
-
if (isReconnecting)
|
|
151
|
-
return; // already reconnecting, skip this tick
|
|
152
|
-
const callbackUrl = buildCallbackUrl(actualPort);
|
|
153
|
-
try {
|
|
154
|
-
await client.registerCallback(callbackUrl, callbackToken);
|
|
155
|
-
// Silent success — no log spam when healthy
|
|
156
|
-
}
|
|
157
|
-
catch (err) {
|
|
158
|
-
if (isReconnecting)
|
|
159
|
-
return;
|
|
160
|
-
isReconnecting = true;
|
|
161
|
-
console.warn(`[E-Claw][${accountId}] Health check failed — starting reconnect loop:`, err);
|
|
162
|
-
let reconnBackoff = BACKOFF_INITIAL_MS;
|
|
163
|
-
let reconnAttempt = 0;
|
|
164
|
-
while (!signal?.aborted) {
|
|
165
|
-
reconnAttempt++;
|
|
166
|
-
const delay = jitter(Math.min(reconnBackoff, BACKOFF_MAX_MS));
|
|
167
|
-
console.warn(`[E-Claw][${accountId}] Reconnect attempt #${reconnAttempt} in ${Math.round(delay / 1000)}s...`);
|
|
168
|
-
await sleep(delay, signal);
|
|
169
|
-
if (signal?.aborted)
|
|
170
|
-
break;
|
|
171
|
-
const recovered = await attemptSetup();
|
|
172
|
-
if (recovered) {
|
|
173
|
-
console.log(`[E-Claw][${accountId}] Reconnected successfully after ${reconnAttempt} attempt(s)!`);
|
|
174
|
-
break;
|
|
175
|
-
}
|
|
176
|
-
reconnBackoff = Math.min(reconnBackoff * BACKOFF_MULTIPLIER, BACKOFF_MAX_MS);
|
|
177
|
-
}
|
|
178
|
-
isReconnecting = false;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
const healthTimer = setInterval(() => { void runHealthCheck(); }, HEALTH_CHECK_INTERVAL_MS);
|
|
182
|
-
// ── Cleanup on abort ────────────────────────────────────────────────
|
|
183
|
-
if (signal) {
|
|
184
|
-
signal.addEventListener('abort', () => {
|
|
185
|
-
console.log(`[E-Claw][${accountId}] Shutting down account`);
|
|
186
|
-
clearInterval(healthTimer);
|
|
187
|
-
client.unregisterCallback().catch(() => { });
|
|
188
|
-
server.close();
|
|
189
|
-
resolve();
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
});
|
|
193
117
|
});
|
|
194
118
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,24 +1,3 @@
|
|
|
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
1
|
declare const plugin: {
|
|
23
2
|
id: string;
|
|
24
3
|
name: string;
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { setPluginRuntime } from './runtime.js';
|
|
2
2
|
import { eclawChannel } from './channel.js';
|
|
3
|
+
import { dispatchWebhook } from './webhook-registry.js';
|
|
3
4
|
/**
|
|
4
5
|
* E-Claw Channel Plugin for OpenClaw.
|
|
5
6
|
*
|
|
@@ -12,24 +13,52 @@ import { eclawChannel } from './channel.js';
|
|
|
12
13
|
* accounts:
|
|
13
14
|
* default:
|
|
14
15
|
* apiKey: "eck_..."
|
|
15
|
-
* apiSecret: "ecs_..."
|
|
16
16
|
* apiBase: "https://eclawbot.com"
|
|
17
17
|
* entityId: 0
|
|
18
18
|
* botName: "My Bot"
|
|
19
|
+
* webhookUrl: "https://your-openclaw-domain.com"
|
|
19
20
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
21
|
+
* The plugin registers /eclaw-webhook on the main OpenClaw gateway HTTP server,
|
|
22
|
+
* so no separate port is needed. Set webhookUrl to your OpenClaw public URL
|
|
23
|
+
* (e.g. https://eclaw2.zeabur.app) so E-Claw knows where to push messages.
|
|
23
24
|
*/
|
|
25
|
+
/** Parse JSON body from a raw incoming request */
|
|
26
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
27
|
+
function parseBody(req) {
|
|
28
|
+
return new Promise((resolve) => {
|
|
29
|
+
let body = '';
|
|
30
|
+
req.on('data', (chunk) => { body += chunk.toString(); });
|
|
31
|
+
req.on('end', () => {
|
|
32
|
+
try {
|
|
33
|
+
req.body = JSON.parse(body);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
req.body = {};
|
|
37
|
+
}
|
|
38
|
+
resolve();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
}
|
|
24
42
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
25
43
|
const plugin = {
|
|
26
|
-
id: '
|
|
44
|
+
id: 'openclaw-channel',
|
|
27
45
|
name: 'E-Claw',
|
|
28
|
-
description: 'E-Claw AI
|
|
46
|
+
description: 'E-Claw AI chat platform channel plugin',
|
|
29
47
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
30
48
|
register(api) {
|
|
31
49
|
console.log('[E-Claw] Plugin loaded');
|
|
32
50
|
setPluginRuntime(api.runtime);
|
|
51
|
+
// Register /eclaw-webhook on the main OpenClaw gateway HTTP server.
|
|
52
|
+
// Token-based routing is handled in dispatchWebhook() — each account
|
|
53
|
+
// registers its own handler keyed by a random per-session Bearer token.
|
|
54
|
+
api.registerHttpRoute({
|
|
55
|
+
path: '/eclaw-webhook',
|
|
56
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
57
|
+
handler: async (req, res) => {
|
|
58
|
+
await parseBody(req);
|
|
59
|
+
await dispatchWebhook(req, res);
|
|
60
|
+
},
|
|
61
|
+
});
|
|
33
62
|
api.registerChannel({ plugin: eclawChannel });
|
|
34
63
|
},
|
|
35
64
|
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export declare const eclawOnboardingAdapter: {
|
|
2
|
+
channel: string;
|
|
3
|
+
getStatus: ({ cfg }: {
|
|
4
|
+
cfg: any;
|
|
5
|
+
}) => Promise<{
|
|
6
|
+
channel: string;
|
|
7
|
+
configured: boolean;
|
|
8
|
+
statusLines: string[];
|
|
9
|
+
selectionHint: string;
|
|
10
|
+
quickstartScore: number;
|
|
11
|
+
}>;
|
|
12
|
+
configure: ({ cfg, prompter }: {
|
|
13
|
+
cfg: any;
|
|
14
|
+
prompter: any;
|
|
15
|
+
}) => Promise<{
|
|
16
|
+
cfg: any;
|
|
17
|
+
accountId: string;
|
|
18
|
+
}>;
|
|
19
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { listAccountIds, resolveAccount } from './config.js';
|
|
2
|
+
const DEFAULT_ACCOUNT_ID = 'default';
|
|
3
|
+
export const eclawOnboardingAdapter = {
|
|
4
|
+
channel: 'eclaw',
|
|
5
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
6
|
+
getStatus: async ({ cfg }) => {
|
|
7
|
+
const ids = listAccountIds(cfg);
|
|
8
|
+
const configured = ids.some((id) => {
|
|
9
|
+
const acc = resolveAccount(cfg, id);
|
|
10
|
+
return Boolean(acc.apiKey);
|
|
11
|
+
});
|
|
12
|
+
return {
|
|
13
|
+
channel: 'eclaw',
|
|
14
|
+
configured,
|
|
15
|
+
statusLines: [`E-Claw: ${configured ? 'configured' : 'not configured'}`],
|
|
16
|
+
selectionHint: configured ? 'configured' : 'E-Claw (AI Live Wallpaper Chat)',
|
|
17
|
+
quickstartScore: configured ? 1 : 3,
|
|
18
|
+
};
|
|
19
|
+
},
|
|
20
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
21
|
+
configure: async ({ cfg, prompter }) => {
|
|
22
|
+
const accountId = DEFAULT_ACCOUNT_ID;
|
|
23
|
+
const resolved = resolveAccount(cfg, accountId);
|
|
24
|
+
await prompter.note([
|
|
25
|
+
'1. Log in to https://eclawbot.com',
|
|
26
|
+
'2. Go to Portal → Settings → Channel API',
|
|
27
|
+
'3. Create an API Key',
|
|
28
|
+
'4. Enter the credentials below',
|
|
29
|
+
].join('\n'), 'E-Claw Setup');
|
|
30
|
+
const apiKey = await prompter.text({
|
|
31
|
+
message: 'Channel API Key',
|
|
32
|
+
placeholder: 'eck_...',
|
|
33
|
+
initialValue: resolved.apiKey || '',
|
|
34
|
+
validate: (v) => (String(v ?? '').trim() ? undefined : 'Required'),
|
|
35
|
+
});
|
|
36
|
+
const entityIdStr = await prompter.text({
|
|
37
|
+
message: 'Entity ID (0–3)',
|
|
38
|
+
placeholder: '0',
|
|
39
|
+
initialValue: String(resolved.entityId ?? 0),
|
|
40
|
+
validate: (v) => {
|
|
41
|
+
const n = Number(v);
|
|
42
|
+
return Number.isInteger(n) && n >= 0 && n <= 3 ? undefined : 'Must be 0–3';
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
const botName = await prompter.text({
|
|
46
|
+
message: 'Bot display name (optional)',
|
|
47
|
+
placeholder: 'My Bot',
|
|
48
|
+
initialValue: resolved.botName ?? '',
|
|
49
|
+
});
|
|
50
|
+
const webhookUrl = await prompter.text({
|
|
51
|
+
message: 'Webhook URL (your OpenClaw public URL, e.g. https://openclaw.example.com)',
|
|
52
|
+
placeholder: 'https://your-openclaw-domain.com',
|
|
53
|
+
initialValue: resolved.webhookUrl ?? '',
|
|
54
|
+
});
|
|
55
|
+
const nextCfg = {
|
|
56
|
+
...cfg,
|
|
57
|
+
channels: {
|
|
58
|
+
...(cfg.channels ?? {}),
|
|
59
|
+
eclaw: {
|
|
60
|
+
...(cfg.channels?.eclaw ?? {}),
|
|
61
|
+
accounts: {
|
|
62
|
+
...(cfg.channels?.eclaw?.accounts ?? {}), // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
63
|
+
[accountId]: {
|
|
64
|
+
apiKey: String(apiKey).trim(),
|
|
65
|
+
apiBase: resolved.apiBase || 'https://eclawbot.com',
|
|
66
|
+
entityId: Number(entityIdStr),
|
|
67
|
+
botName: String(botName).trim() || undefined,
|
|
68
|
+
webhookUrl: String(webhookUrl).trim() || undefined,
|
|
69
|
+
enabled: true,
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
return { cfg: nextCfg, accountId };
|
|
76
|
+
},
|
|
77
|
+
};
|
package/dist/outbound.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { EClawClient } from './client.js';
|
|
2
|
+
export declare function setActiveEvent(accountId: string, event: string): void;
|
|
3
|
+
export declare function clearActiveEvent(accountId: string): void;
|
|
2
4
|
export declare function setClient(accountId: string, client: EClawClient): void;
|
|
3
5
|
export declare function getClient(accountId: string): EClawClient | undefined;
|
|
4
6
|
/** OpenClaw outbound: send text message to E-Claw user */
|
package/dist/outbound.js
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
/** Client instances keyed by accountId */
|
|
2
2
|
const clients = new Map();
|
|
3
|
+
/** Track current inbound event type per account to suppress duplicate sendMessage calls */
|
|
4
|
+
const activeEvent = new Map();
|
|
5
|
+
export function setActiveEvent(accountId, event) {
|
|
6
|
+
activeEvent.set(accountId, event);
|
|
7
|
+
}
|
|
8
|
+
export function clearActiveEvent(accountId) {
|
|
9
|
+
activeEvent.delete(accountId);
|
|
10
|
+
}
|
|
3
11
|
export function setClient(accountId, client) {
|
|
4
12
|
clients.set(accountId, client);
|
|
5
13
|
}
|
|
@@ -10,6 +18,11 @@ export function getClient(accountId) {
|
|
|
10
18
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
11
19
|
export async function sendText(ctx) {
|
|
12
20
|
const accountId = ctx.accountId ?? 'default';
|
|
21
|
+
// Suppress duplicate delivery for bot-to-bot events — webhook-handler's deliver handles these
|
|
22
|
+
const event = activeEvent.get(accountId) ?? 'message';
|
|
23
|
+
if (event === 'entity_message' || event === 'broadcast') {
|
|
24
|
+
return { channel: 'eclaw', messageId: '', chatId: '' };
|
|
25
|
+
}
|
|
13
26
|
const client = clients.get(accountId);
|
|
14
27
|
if (!client) {
|
|
15
28
|
return { channel: 'eclaw', messageId: '', chatId: '' };
|
|
@@ -32,6 +45,11 @@ export async function sendText(ctx) {
|
|
|
32
45
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
33
46
|
export async function sendMedia(ctx) {
|
|
34
47
|
const accountId = ctx.accountId ?? 'default';
|
|
48
|
+
// Suppress duplicate delivery for bot-to-bot events — webhook-handler's deliver handles these
|
|
49
|
+
const event = activeEvent.get(accountId) ?? 'message';
|
|
50
|
+
if (event === 'entity_message' || event === 'broadcast') {
|
|
51
|
+
return { channel: 'eclaw', messageId: '', chatId: '' };
|
|
52
|
+
}
|
|
35
53
|
const client = clients.get(accountId);
|
|
36
54
|
if (!client) {
|
|
37
55
|
return { channel: 'eclaw', messageId: '', chatId: '' };
|
package/dist/types.d.ts
CHANGED
|
@@ -2,10 +2,19 @@
|
|
|
2
2
|
export interface EClawAccountConfig {
|
|
3
3
|
enabled: boolean;
|
|
4
4
|
apiKey: string;
|
|
5
|
-
apiSecret
|
|
5
|
+
apiSecret?: string;
|
|
6
6
|
apiBase: string;
|
|
7
|
-
entityId
|
|
7
|
+
entityId?: number;
|
|
8
8
|
botName?: string;
|
|
9
|
+
webhookUrl?: string;
|
|
10
|
+
}
|
|
11
|
+
/** Context block injected by E-Claw server for Channel Bot parity with Traditional Bot */
|
|
12
|
+
export interface EClawContext {
|
|
13
|
+
b2bRemaining?: number;
|
|
14
|
+
b2bMax?: number;
|
|
15
|
+
expectsReply?: boolean;
|
|
16
|
+
missionHints?: string;
|
|
17
|
+
silentToken?: string;
|
|
9
18
|
}
|
|
10
19
|
/** Inbound message from E-Claw callback webhook */
|
|
11
20
|
export interface EClawInboundMessage {
|
|
@@ -24,6 +33,7 @@ export interface EClawInboundMessage {
|
|
|
24
33
|
fromEntityId?: number;
|
|
25
34
|
fromCharacter?: string;
|
|
26
35
|
fromPublicCode?: string;
|
|
36
|
+
eclaw_context?: EClawContext;
|
|
27
37
|
}
|
|
28
38
|
/** Entity info returned by channel register */
|
|
29
39
|
export interface EClawEntityInfo {
|
|
@@ -49,6 +59,13 @@ export interface BindResponse {
|
|
|
49
59
|
publicCode: string;
|
|
50
60
|
bindingType: string;
|
|
51
61
|
}
|
|
62
|
+
/** Error response when all entity slots are full */
|
|
63
|
+
export interface SlotsFullError {
|
|
64
|
+
success: false;
|
|
65
|
+
message: string;
|
|
66
|
+
entities: EClawEntityInfo[];
|
|
67
|
+
hint: string;
|
|
68
|
+
}
|
|
52
69
|
/** Response from POST /api/channel/message */
|
|
53
70
|
export interface MessageResponse {
|
|
54
71
|
success: boolean;
|
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Create an HTTP request handler for inbound messages from E-Claw.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Handles three event types:
|
|
5
|
+
* - 'message' → Normal human message; reply via sendMessage()
|
|
6
|
+
* - 'entity_message' → Bot-to-bot speak-to; reply via sendMessage() + speakTo(fromEntityId)
|
|
7
|
+
* - 'broadcast' → Broadcast from another entity; reply via sendMessage() + speakTo(fromEntityId)
|
|
8
|
+
*
|
|
9
|
+
* The `deliver` callback routes AI response to the correct E-Claw endpoint
|
|
10
|
+
* based on the inbound event type.
|
|
11
|
+
*
|
|
12
|
+
* Channel Bot Context Parity v1.0.17:
|
|
13
|
+
* - Bot-to-bot / broadcast now calls sendMessage() to update own wallpaper AND speakTo() to reply
|
|
14
|
+
* - Quota awareness via eclaw_context.b2bRemaining / b2bMax
|
|
15
|
+
* - Mission context via eclaw_context.missionHints
|
|
16
|
+
* - Silent suppression via silentToken (default "[SILENT]")
|
|
6
17
|
*/
|
|
7
|
-
export declare function createWebhookHandler(expectedToken: string, accountId: string): (req: any, res: any) => Promise<void>;
|
|
18
|
+
export declare function createWebhookHandler(expectedToken: string, accountId: string, cfg: any): (req: any, res: any) => Promise<void>;
|