clawrelay 0.1.0 → 0.2.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/package.json +1 -1
- package/src/channel.ts +3 -0
- package/src/onboarding.ts +251 -0
package/package.json
CHANGED
package/src/channel.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import type { RelayAccount, RelayInboundMessage, RelayOutboundMessage, ReplyFn } from './types.js';
|
|
4
4
|
import { InboundServer } from './inbound-server.js';
|
|
5
5
|
import { getRelayRuntime } from './runtime.js';
|
|
6
|
+
import { relayOnboardingAdapter } from './onboarding.js';
|
|
6
7
|
|
|
7
8
|
const CHANNEL_ID = 'relay' as const;
|
|
8
9
|
|
|
@@ -24,6 +25,8 @@ export function createRelayChannel(api: any) {
|
|
|
24
25
|
aliases: ['relay'],
|
|
25
26
|
},
|
|
26
27
|
|
|
28
|
+
onboarding: relayOnboardingAdapter,
|
|
29
|
+
|
|
27
30
|
capabilities: {
|
|
28
31
|
chatTypes: ['direct', 'group'],
|
|
29
32
|
media: {
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ChannelOnboardingAdapter,
|
|
3
|
+
OpenClawConfig,
|
|
4
|
+
WizardPrompter,
|
|
5
|
+
} from "openclaw/plugin-sdk";
|
|
6
|
+
import {
|
|
7
|
+
DEFAULT_ACCOUNT_ID,
|
|
8
|
+
normalizeAccountId,
|
|
9
|
+
promptAccountId,
|
|
10
|
+
} from "openclaw/plugin-sdk";
|
|
11
|
+
import crypto from "node:crypto";
|
|
12
|
+
|
|
13
|
+
const channel = "relay" as const;
|
|
14
|
+
|
|
15
|
+
function generateAuthToken(): string {
|
|
16
|
+
const bytes = crypto.randomBytes(24);
|
|
17
|
+
return `crly_${bytes.toString("base64url")}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function setRelayAccountConfig(
|
|
21
|
+
cfg: OpenClawConfig,
|
|
22
|
+
accountId: string,
|
|
23
|
+
defaultPatch: Record<string, unknown>,
|
|
24
|
+
accountPatch: Record<string, unknown> = defaultPatch,
|
|
25
|
+
): OpenClawConfig {
|
|
26
|
+
if (accountId === DEFAULT_ACCOUNT_ID) {
|
|
27
|
+
return {
|
|
28
|
+
...cfg,
|
|
29
|
+
channels: {
|
|
30
|
+
...cfg.channels,
|
|
31
|
+
relay: {
|
|
32
|
+
...cfg.channels?.relay,
|
|
33
|
+
enabled: true,
|
|
34
|
+
...defaultPatch,
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
} as OpenClawConfig;
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
...cfg,
|
|
41
|
+
channels: {
|
|
42
|
+
...cfg.channels,
|
|
43
|
+
relay: {
|
|
44
|
+
...cfg.channels?.relay,
|
|
45
|
+
enabled: true,
|
|
46
|
+
accounts: {
|
|
47
|
+
...cfg.channels?.relay?.accounts,
|
|
48
|
+
[accountId]: {
|
|
49
|
+
...cfg.channels?.relay?.accounts?.[accountId],
|
|
50
|
+
enabled: cfg.channels?.relay?.accounts?.[accountId]?.enabled ?? true,
|
|
51
|
+
...accountPatch,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
} as OpenClawConfig;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function listRelayAccountIds(cfg: OpenClawConfig): string[] {
|
|
60
|
+
return Object.keys((cfg.channels as any)?.relay?.accounts ?? {});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function resolveDefaultRelayAccountId(cfg: OpenClawConfig): string {
|
|
64
|
+
const ids = listRelayAccountIds(cfg);
|
|
65
|
+
if (ids.includes(DEFAULT_ACCOUNT_ID)) return DEFAULT_ACCOUNT_ID;
|
|
66
|
+
return ids[0] ?? DEFAULT_ACCOUNT_ID;
|
|
67
|
+
}
|
|
68
|
+
|
|
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
|
+
function getExistingPort(cfg: OpenClawConfig, accountId: string): number | undefined {
|
|
78
|
+
const relay = (cfg.channels as any)?.relay;
|
|
79
|
+
if (accountId === DEFAULT_ACCOUNT_ID) {
|
|
80
|
+
return relay?.port;
|
|
81
|
+
}
|
|
82
|
+
return relay?.accounts?.[accountId]?.port;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const relayOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
86
|
+
channel,
|
|
87
|
+
|
|
88
|
+
getStatus: async ({ cfg }) => {
|
|
89
|
+
const ids = listRelayAccountIds(cfg);
|
|
90
|
+
const relay = (cfg.channels as any)?.relay;
|
|
91
|
+
|
|
92
|
+
// Check default account (top-level authToken) or any named account
|
|
93
|
+
let configured = Boolean(relay?.authToken);
|
|
94
|
+
if (!configured) {
|
|
95
|
+
for (const id of ids) {
|
|
96
|
+
if (relay?.accounts?.[id]?.authToken) {
|
|
97
|
+
configured = true;
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
channel,
|
|
105
|
+
configured,
|
|
106
|
+
statusLines: [
|
|
107
|
+
`Channel Relay: ${configured ? "configured" : "needs setup"}`,
|
|
108
|
+
],
|
|
109
|
+
selectionHint: configured
|
|
110
|
+
? "configured"
|
|
111
|
+
: "proxy for Discord/Telegram",
|
|
112
|
+
quickstartScore: configured ? 1 : 20,
|
|
113
|
+
};
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
configure: async ({
|
|
117
|
+
cfg,
|
|
118
|
+
prompter,
|
|
119
|
+
accountOverrides,
|
|
120
|
+
shouldPromptAccountIds,
|
|
121
|
+
}) => {
|
|
122
|
+
await prompter.note(
|
|
123
|
+
[
|
|
124
|
+
"ClawRelay bridges Discord/Telegram to OpenClaw via a",
|
|
125
|
+
"lightweight always-on relay service. This wizard configures",
|
|
126
|
+
"the channel plugin side (auth token + port).",
|
|
127
|
+
"",
|
|
128
|
+
"You'll deploy the relay service separately afterwards.",
|
|
129
|
+
].join("\n"),
|
|
130
|
+
"Channel Relay Setup",
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
// --- Account ID ---
|
|
134
|
+
const relayOverride = (accountOverrides as any).relay?.trim();
|
|
135
|
+
const defaultAccountId = resolveDefaultRelayAccountId(cfg);
|
|
136
|
+
let accountId = relayOverride
|
|
137
|
+
? normalizeAccountId(relayOverride)
|
|
138
|
+
: defaultAccountId;
|
|
139
|
+
|
|
140
|
+
if (shouldPromptAccountIds && !relayOverride) {
|
|
141
|
+
accountId = await promptAccountId({
|
|
142
|
+
cfg,
|
|
143
|
+
prompter,
|
|
144
|
+
label: "Channel Relay",
|
|
145
|
+
currentId: accountId,
|
|
146
|
+
listAccountIds: listRelayAccountIds,
|
|
147
|
+
defaultAccountId,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
let next = cfg;
|
|
152
|
+
|
|
153
|
+
// --- Auth Token ---
|
|
154
|
+
const existingToken = getExistingToken(next, accountId);
|
|
155
|
+
let authToken: string;
|
|
156
|
+
|
|
157
|
+
if (existingToken) {
|
|
158
|
+
const keepToken = await prompter.confirm({
|
|
159
|
+
message: `Auth token already set (${existingToken.slice(0, 12)}...). Keep it?`,
|
|
160
|
+
initialValue: true,
|
|
161
|
+
});
|
|
162
|
+
if (keepToken) {
|
|
163
|
+
authToken = existingToken;
|
|
164
|
+
} else {
|
|
165
|
+
authToken = await promptAuthToken(prompter);
|
|
166
|
+
}
|
|
167
|
+
} else {
|
|
168
|
+
authToken = await promptAuthToken(prompter);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// --- Port ---
|
|
172
|
+
const existingPort = getExistingPort(next, accountId);
|
|
173
|
+
let port: number;
|
|
174
|
+
|
|
175
|
+
if (existingPort) {
|
|
176
|
+
const keepPort = await prompter.confirm({
|
|
177
|
+
message: `Inbound port is ${existingPort}. Keep it?`,
|
|
178
|
+
initialValue: true,
|
|
179
|
+
});
|
|
180
|
+
if (keepPort) {
|
|
181
|
+
port = existingPort;
|
|
182
|
+
} else {
|
|
183
|
+
port = await promptPort(prompter);
|
|
184
|
+
}
|
|
185
|
+
} else {
|
|
186
|
+
port = await promptPort(prompter);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// --- Write config ---
|
|
190
|
+
next = setRelayAccountConfig(next, accountId, {
|
|
191
|
+
authToken,
|
|
192
|
+
port,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// --- Next steps ---
|
|
196
|
+
await prompter.note(
|
|
197
|
+
[
|
|
198
|
+
"Plugin configured! Now deploy the relay service.",
|
|
199
|
+
"",
|
|
200
|
+
"Required env vars for the relay service:",
|
|
201
|
+
` DISCORD_TOKEN=<your-bot-token>`,
|
|
202
|
+
` SANDBOX_PLUGIN_URL=ws://localhost:${port}`,
|
|
203
|
+
` SANDBOX_AUTH_TOKEN=${authToken}`,
|
|
204
|
+
"",
|
|
205
|
+
"Quick start:",
|
|
206
|
+
` docker run -e DISCORD_TOKEN=... -e SANDBOX_PLUGIN_URL=ws://host.docker.internal:${port} -e SANDBOX_AUTH_TOKEN=${authToken} ghcr.io/kylemclaren/clawrelay:latest`,
|
|
207
|
+
"",
|
|
208
|
+
"Docs: https://github.com/kylemclaren/clawrelay",
|
|
209
|
+
].join("\n"),
|
|
210
|
+
"Next Steps",
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
return { cfg: next, accountId };
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
async function promptAuthToken(prompter: WizardPrompter): Promise<string> {
|
|
218
|
+
const generated = generateAuthToken();
|
|
219
|
+
const useGenerated = await prompter.confirm({
|
|
220
|
+
message: `Use auto-generated token? (${generated})`,
|
|
221
|
+
initialValue: true,
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
if (useGenerated) {
|
|
225
|
+
return generated;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const custom = await prompter.text({
|
|
229
|
+
message: "Enter your auth token",
|
|
230
|
+
validate: (value) =>
|
|
231
|
+
String(value ?? "").trim().length >= 8
|
|
232
|
+
? undefined
|
|
233
|
+
: "Token must be at least 8 characters",
|
|
234
|
+
});
|
|
235
|
+
return String(custom).trim();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function promptPort(prompter: WizardPrompter): Promise<number> {
|
|
239
|
+
const portStr = await prompter.text({
|
|
240
|
+
message: "Inbound server port",
|
|
241
|
+
initialValue: "7600",
|
|
242
|
+
validate: (value) => {
|
|
243
|
+
const n = Number(value);
|
|
244
|
+
if (!Number.isInteger(n) || n < 1 || n > 65535) {
|
|
245
|
+
return "Enter a valid port (1-65535)";
|
|
246
|
+
}
|
|
247
|
+
return undefined;
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
return Number(portStr);
|
|
251
|
+
}
|