clawrelay 0.3.3 → 0.3.5
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 +9 -7
- package/package.json +1 -1
- package/src/channel.ts +0 -1
- package/src/gateway-handler.ts +67 -23
- package/src/onboarding.ts +102 -76
- package/src/types.ts +0 -1
package/README.md
CHANGED
|
@@ -24,6 +24,12 @@ Or install from a local path during development:
|
|
|
24
24
|
openclaw plugins install ./packages/relay-channel
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
+
Update to the latest version:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
openclaw plugins update clawrelay
|
|
31
|
+
```
|
|
32
|
+
|
|
27
33
|
Or install from a tarball:
|
|
28
34
|
|
|
29
35
|
```bash
|
|
@@ -39,11 +45,7 @@ Add to `~/.openclaw/openclaw.json`:
|
|
|
39
45
|
{
|
|
40
46
|
"channels": {
|
|
41
47
|
"relay": {
|
|
42
|
-
"
|
|
43
|
-
"default": {
|
|
44
|
-
"authToken": "optional-per-account-token"
|
|
45
|
-
}
|
|
46
|
-
}
|
|
48
|
+
"enabled": true
|
|
47
49
|
}
|
|
48
50
|
},
|
|
49
51
|
"plugins": {
|
|
@@ -55,7 +57,7 @@ Add to `~/.openclaw/openclaw.json`:
|
|
|
55
57
|
}
|
|
56
58
|
```
|
|
57
59
|
|
|
58
|
-
|
|
60
|
+
Authentication is handled by the gateway's own auth token (`gateway.auth.token`).
|
|
59
61
|
|
|
60
62
|
## Verify installation
|
|
61
63
|
|
|
@@ -80,7 +82,7 @@ openclaw plugins doctor
|
|
|
80
82
|
| `src/channel.ts` | Channel definition, config resolution, onboarding |
|
|
81
83
|
| `src/gateway-handler.ts` | `relay.inbound` gateway method handler |
|
|
82
84
|
| `src/health-handler.ts` | HTTP health route handler |
|
|
83
|
-
| `src/onboarding.ts` | Setup wizard (
|
|
85
|
+
| `src/onboarding.ts` | Setup wizard (enable channel, sprite gateway service) |
|
|
84
86
|
| `src/runtime.ts` | OpenClaw runtime accessor |
|
|
85
87
|
| `src/types.ts` | Protocol and config types |
|
|
86
88
|
|
package/package.json
CHANGED
package/src/channel.ts
CHANGED
package/src/gateway-handler.ts
CHANGED
|
@@ -23,7 +23,7 @@ export function createRelayInboundHandler(api: any) {
|
|
|
23
23
|
respond: (ok: boolean, payload?: unknown, error?: unknown) => void;
|
|
24
24
|
context: any;
|
|
25
25
|
}) => {
|
|
26
|
-
const { params, respond } = opts;
|
|
26
|
+
const { params, client, respond } = opts;
|
|
27
27
|
|
|
28
28
|
const message = params.message as RelayInboundMessage | undefined;
|
|
29
29
|
if (!message || !message.messageId || !message.content) {
|
|
@@ -50,7 +50,6 @@ export function createRelayInboundHandler(api: any) {
|
|
|
50
50
|
|
|
51
51
|
const account: RelayAccount = {
|
|
52
52
|
accountId,
|
|
53
|
-
authToken: accountData.authToken ?? '',
|
|
54
53
|
enabled: accountData.enabled !== false,
|
|
55
54
|
};
|
|
56
55
|
|
|
@@ -62,26 +61,69 @@ export function createRelayInboundHandler(api: any) {
|
|
|
62
61
|
return;
|
|
63
62
|
}
|
|
64
63
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
64
|
+
const streaming = params.streaming === true;
|
|
65
|
+
|
|
66
|
+
if (streaming) {
|
|
67
|
+
// Streaming mode: ack immediately, then send deltas via events
|
|
68
|
+
respond(true, { messageId: message.messageId, streaming: true });
|
|
69
|
+
|
|
70
|
+
const streamCallback = (text: string) => {
|
|
71
|
+
try {
|
|
72
|
+
client.sendEvent('relay.stream.delta', {
|
|
73
|
+
messageId: message.messageId,
|
|
74
|
+
text,
|
|
75
|
+
kind: 'deliver',
|
|
76
|
+
});
|
|
77
|
+
} catch (err) {
|
|
78
|
+
logger.debug(`[clawrelay] Failed to send stream delta: ${err}`);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const responseContent = await processRelayMessage({
|
|
84
|
+
message,
|
|
85
|
+
account,
|
|
86
|
+
config,
|
|
87
|
+
log: logger,
|
|
88
|
+
streamCallback,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
client.sendEvent('relay.stream.done', {
|
|
92
|
+
messageId: message.messageId,
|
|
93
|
+
text: responseContent,
|
|
94
|
+
});
|
|
95
|
+
} catch (err) {
|
|
96
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
97
|
+
logger.error(`[clawrelay] Failed to process inbound (streaming): ${errMsg}`);
|
|
98
|
+
client.sendEvent('relay.stream.done', {
|
|
99
|
+
messageId: message.messageId,
|
|
100
|
+
text: '',
|
|
101
|
+
error: `Error processing message: ${errMsg}`,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
// Non-streaming: buffered response as before
|
|
106
|
+
try {
|
|
107
|
+
const responseContent = await processRelayMessage({
|
|
108
|
+
message,
|
|
109
|
+
account,
|
|
110
|
+
config,
|
|
111
|
+
log: logger,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
respond(true, {
|
|
115
|
+
messageId: message.messageId,
|
|
116
|
+
content: responseContent,
|
|
117
|
+
replyToMessageId: message.messageId,
|
|
118
|
+
});
|
|
119
|
+
} catch (err) {
|
|
120
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
121
|
+
logger.error(`[clawrelay] Failed to process inbound: ${errMsg}`);
|
|
122
|
+
respond(false, undefined, {
|
|
123
|
+
code: 'INTERNAL_ERROR',
|
|
124
|
+
message: `Error processing message: ${errMsg}`,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
85
127
|
}
|
|
86
128
|
};
|
|
87
129
|
}
|
|
@@ -108,8 +150,9 @@ async function processRelayMessage(params: {
|
|
|
108
150
|
account: RelayAccount;
|
|
109
151
|
config: any;
|
|
110
152
|
log: any;
|
|
153
|
+
streamCallback?: (text: string) => void;
|
|
111
154
|
}): Promise<string> {
|
|
112
|
-
const { message, account, config, log } = params;
|
|
155
|
+
const { message, account, config, log, streamCallback } = params;
|
|
113
156
|
|
|
114
157
|
const core = getRelayRuntime();
|
|
115
158
|
const peerId = resolvePeerId(message);
|
|
@@ -190,6 +233,7 @@ async function processRelayMessage(params: {
|
|
|
190
233
|
const text = payload.text ?? '';
|
|
191
234
|
if (text.trim()) {
|
|
192
235
|
parts.push(text);
|
|
236
|
+
streamCallback?.(text);
|
|
193
237
|
}
|
|
194
238
|
},
|
|
195
239
|
onError: (err: unknown, info: { kind: string }) => {
|
package/src/onboarding.ts
CHANGED
|
@@ -8,20 +8,107 @@ import {
|
|
|
8
8
|
normalizeAccountId,
|
|
9
9
|
promptAccountId,
|
|
10
10
|
} from "openclaw/plugin-sdk";
|
|
11
|
-
import
|
|
11
|
+
import { execSync } from "node:child_process";
|
|
12
|
+
import path from "node:path";
|
|
12
13
|
|
|
13
14
|
const channel = "relay" as const;
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
const SPRITE_ENV_BIN = "/.sprite/bin/sprite-env";
|
|
17
|
+
const DEFAULT_GATEWAY_PORT = 18789;
|
|
18
|
+
const GATEWAY_SERVICE_NAME = "openclaw-gateway";
|
|
19
|
+
|
|
20
|
+
function isOnSprite(): boolean {
|
|
21
|
+
try {
|
|
22
|
+
execSync(`test -x ${SPRITE_ENV_BIN}`, { stdio: "ignore" });
|
|
23
|
+
return true;
|
|
24
|
+
} catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getExistingGatewayService(): Record<string, unknown> | undefined {
|
|
30
|
+
try {
|
|
31
|
+
const out = execSync(`${SPRITE_ENV_BIN} services list`, {
|
|
32
|
+
encoding: "utf-8",
|
|
33
|
+
timeout: 5000,
|
|
34
|
+
}).trim();
|
|
35
|
+
const services: Record<string, unknown>[] = JSON.parse(out);
|
|
36
|
+
return services.find(
|
|
37
|
+
(s) => (s as any).name === GATEWAY_SERVICE_NAME,
|
|
38
|
+
);
|
|
39
|
+
} catch {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function findOpenclawBinary(): string | undefined {
|
|
45
|
+
const binDir = path.dirname(process.execPath);
|
|
46
|
+
const candidate = path.join(binDir, "openclaw");
|
|
47
|
+
try {
|
|
48
|
+
execSync(`test -x "${candidate}"`, { stdio: "ignore" });
|
|
49
|
+
return candidate;
|
|
50
|
+
} catch {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function ensureSpriteGatewayService(
|
|
56
|
+
cfg: OpenClawConfig,
|
|
57
|
+
prompter: WizardPrompter,
|
|
58
|
+
): Promise<void> {
|
|
59
|
+
if (!isOnSprite()) return;
|
|
60
|
+
|
|
61
|
+
const existing = getExistingGatewayService();
|
|
62
|
+
if (existing) {
|
|
63
|
+
await prompter.note(
|
|
64
|
+
`Sprite gateway service already exists (${GATEWAY_SERVICE_NAME}).`,
|
|
65
|
+
"Sprite",
|
|
66
|
+
);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const openclawBin = findOpenclawBinary();
|
|
71
|
+
if (!openclawBin) {
|
|
72
|
+
await prompter.note(
|
|
73
|
+
"Could not locate the openclaw binary to create the gateway service. You may need to create it manually:\n" +
|
|
74
|
+
` sprite-env services create ${GATEWAY_SERVICE_NAME} --cmd openclaw --args "gateway,--port,${DEFAULT_GATEWAY_PORT}" --http-port ${DEFAULT_GATEWAY_PORT}`,
|
|
75
|
+
"Sprite",
|
|
76
|
+
);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const gwCfg = (cfg as any).gateway;
|
|
81
|
+
const port = gwCfg?.port ?? DEFAULT_GATEWAY_PORT;
|
|
82
|
+
|
|
83
|
+
const shouldCreate = await prompter.confirm({
|
|
84
|
+
message: `Create sprite gateway service (${GATEWAY_SERVICE_NAME} on port ${port})?`,
|
|
85
|
+
initialValue: true,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
if (!shouldCreate) return;
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
execSync(
|
|
92
|
+
`${SPRITE_ENV_BIN} services create ${GATEWAY_SERVICE_NAME} --cmd "${openclawBin}" --args "gateway,--port,${port}" --http-port ${port}`,
|
|
93
|
+
{ encoding: "utf-8", timeout: 10000 },
|
|
94
|
+
);
|
|
95
|
+
await prompter.note(
|
|
96
|
+
`Gateway service created: ${GATEWAY_SERVICE_NAME} → ${openclawBin} gateway --port ${port}`,
|
|
97
|
+
"Sprite",
|
|
98
|
+
);
|
|
99
|
+
} catch (err) {
|
|
100
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
101
|
+
await prompter.note(
|
|
102
|
+
`Failed to create gateway service: ${msg}\n\nYou can create it manually:\n` +
|
|
103
|
+
` sprite-env services create ${GATEWAY_SERVICE_NAME} --cmd "${openclawBin}" --args "gateway,--port,${port}" --http-port ${port}`,
|
|
104
|
+
"Sprite",
|
|
105
|
+
);
|
|
106
|
+
}
|
|
18
107
|
}
|
|
19
108
|
|
|
20
109
|
function setRelayAccountConfig(
|
|
21
110
|
cfg: OpenClawConfig,
|
|
22
111
|
accountId: string,
|
|
23
|
-
defaultPatch: Record<string, unknown>,
|
|
24
|
-
accountPatch: Record<string, unknown> = defaultPatch,
|
|
25
112
|
): OpenClawConfig {
|
|
26
113
|
if (accountId === DEFAULT_ACCOUNT_ID) {
|
|
27
114
|
return {
|
|
@@ -31,7 +118,6 @@ function setRelayAccountConfig(
|
|
|
31
118
|
relay: {
|
|
32
119
|
...cfg.channels?.relay,
|
|
33
120
|
enabled: true,
|
|
34
|
-
...defaultPatch,
|
|
35
121
|
},
|
|
36
122
|
},
|
|
37
123
|
} as OpenClawConfig;
|
|
@@ -48,7 +134,6 @@ function setRelayAccountConfig(
|
|
|
48
134
|
[accountId]: {
|
|
49
135
|
...cfg.channels?.relay?.accounts?.[accountId],
|
|
50
136
|
enabled: cfg.channels?.relay?.accounts?.[accountId]?.enabled ?? true,
|
|
51
|
-
...accountPatch,
|
|
52
137
|
},
|
|
53
138
|
},
|
|
54
139
|
},
|
|
@@ -66,31 +151,12 @@ function resolveDefaultRelayAccountId(cfg: OpenClawConfig): string {
|
|
|
66
151
|
return ids[0] ?? DEFAULT_ACCOUNT_ID;
|
|
67
152
|
}
|
|
68
153
|
|
|
69
|
-
function getExistingToken(cfg: OpenClawConfig, accountId: string): string | undefined {
|
|
70
|
-
const relay = (cfg.channels as any)?.relay;
|
|
71
|
-
if (accountId === DEFAULT_ACCOUNT_ID) {
|
|
72
|
-
return relay?.authToken;
|
|
73
|
-
}
|
|
74
|
-
return relay?.accounts?.[accountId]?.authToken;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
154
|
export const relayOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
78
155
|
channel,
|
|
79
156
|
|
|
80
157
|
getStatus: async ({ cfg }) => {
|
|
81
|
-
const ids = listRelayAccountIds(cfg);
|
|
82
158
|
const relay = (cfg.channels as any)?.relay;
|
|
83
|
-
|
|
84
|
-
// Check default account (top-level authToken) or any named account
|
|
85
|
-
let configured = Boolean(relay?.authToken);
|
|
86
|
-
if (!configured) {
|
|
87
|
-
for (const id of ids) {
|
|
88
|
-
if (relay?.accounts?.[id]?.authToken) {
|
|
89
|
-
configured = true;
|
|
90
|
-
break;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
159
|
+
const configured = relay?.enabled === true;
|
|
94
160
|
|
|
95
161
|
return {
|
|
96
162
|
channel,
|
|
@@ -114,11 +180,11 @@ export const relayOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
114
180
|
await prompter.note(
|
|
115
181
|
[
|
|
116
182
|
"ClawRelay bridges Discord/Telegram to OpenClaw via a",
|
|
117
|
-
"lightweight always-on relay service. This wizard
|
|
118
|
-
"the channel plugin
|
|
183
|
+
"lightweight always-on relay service. This wizard enables",
|
|
184
|
+
"the channel plugin and configures the gateway service.",
|
|
119
185
|
"",
|
|
120
186
|
"The relay service connects to the OpenClaw gateway via WS",
|
|
121
|
-
"and
|
|
187
|
+
"and authenticates using the gateway auth token.",
|
|
122
188
|
"",
|
|
123
189
|
"You'll deploy the relay service separately afterwards.",
|
|
124
190
|
].join("\n"),
|
|
@@ -143,57 +209,17 @@ export const relayOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
143
209
|
});
|
|
144
210
|
}
|
|
145
211
|
|
|
146
|
-
let next = cfg;
|
|
147
|
-
|
|
148
|
-
// --- Auth Token (optional per-account verification token) ---
|
|
149
|
-
const existingToken = getExistingToken(next, accountId);
|
|
150
|
-
let authToken: string;
|
|
151
|
-
|
|
152
|
-
if (existingToken) {
|
|
153
|
-
const keepToken = await prompter.confirm({
|
|
154
|
-
message: `Auth token already set (${existingToken.slice(0, 12)}...). Keep it?`,
|
|
155
|
-
initialValue: true,
|
|
156
|
-
});
|
|
157
|
-
if (keepToken) {
|
|
158
|
-
authToken = existingToken;
|
|
159
|
-
} else {
|
|
160
|
-
authToken = await promptAuthToken(prompter);
|
|
161
|
-
}
|
|
162
|
-
} else {
|
|
163
|
-
authToken = await promptAuthToken(prompter);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
212
|
// --- Write config ---
|
|
167
|
-
next = setRelayAccountConfig(
|
|
168
|
-
|
|
169
|
-
|
|
213
|
+
const next = setRelayAccountConfig(cfg, accountId);
|
|
214
|
+
|
|
215
|
+
// --- Sprite gateway service ---
|
|
216
|
+
await ensureSpriteGatewayService(next, prompter);
|
|
170
217
|
|
|
171
218
|
await prompter.note(
|
|
172
|
-
|
|
219
|
+
"Relay channel enabled. Select Finished below, then deploy the relay service.",
|
|
173
220
|
"Relay",
|
|
174
221
|
);
|
|
175
222
|
|
|
176
223
|
return { cfg: next, accountId };
|
|
177
224
|
},
|
|
178
225
|
};
|
|
179
|
-
|
|
180
|
-
async function promptAuthToken(prompter: WizardPrompter): Promise<string> {
|
|
181
|
-
const generated = generateAuthToken();
|
|
182
|
-
const useGenerated = await prompter.confirm({
|
|
183
|
-
message: `Use auto-generated token? (${generated})`,
|
|
184
|
-
initialValue: true,
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
if (useGenerated) {
|
|
188
|
-
return generated;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const custom = await prompter.text({
|
|
192
|
-
message: "Enter your auth token",
|
|
193
|
-
validate: (value) =>
|
|
194
|
-
String(value ?? "").trim().length >= 8
|
|
195
|
-
? undefined
|
|
196
|
-
: "Token must be at least 8 characters",
|
|
197
|
-
});
|
|
198
|
-
return String(custom).trim();
|
|
199
|
-
}
|