@zbruceli/openclaw-dchat 0.2.0 → 0.4.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/.claude/settings.local.json +3 -1
- package/index.ts +2 -2
- package/package.json +1 -1
- package/src/channel.ts +52 -16
- package/src/config-schema.ts +1 -1
- package/src/nkn-bus.ts +213 -1
- package/src/onboarding.ts +116 -135
- package/src/runtime.ts +1 -1
package/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
-
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
|
|
2
|
+
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/core";
|
|
3
3
|
import { dchatPlugin } from "./src/channel.js";
|
|
4
4
|
import { setDchatRuntime } from "./src/runtime.js";
|
|
5
5
|
|
package/package.json
CHANGED
package/src/channel.ts
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
import {
|
|
2
2
|
applyAccountNameToChannelSection,
|
|
3
|
-
buildBaseAccountStatusSnapshot,
|
|
4
|
-
buildBaseChannelStatusSummary,
|
|
5
3
|
buildChannelConfigSchema,
|
|
6
|
-
collectStatusIssuesFromLastError,
|
|
7
|
-
createDefaultChannelRuntimeState,
|
|
8
4
|
DEFAULT_ACCOUNT_ID,
|
|
9
5
|
deleteAccountFromConfigSection,
|
|
10
6
|
normalizeAccountId,
|
|
11
|
-
resolveOutboundMediaUrls,
|
|
12
|
-
resolveSenderCommandAuthorization,
|
|
13
7
|
setAccountEnabledInConfigSection,
|
|
14
8
|
type ChannelPlugin,
|
|
15
|
-
|
|
16
|
-
|
|
9
|
+
type PluginRuntime,
|
|
10
|
+
} from "openclaw/plugin-sdk/core";
|
|
11
|
+
import {
|
|
12
|
+
buildBaseAccountStatusSnapshot,
|
|
13
|
+
buildBaseChannelStatusSummary,
|
|
14
|
+
collectStatusIssuesFromLastError,
|
|
15
|
+
createDefaultChannelRuntimeState,
|
|
16
|
+
} from "openclaw/plugin-sdk/status-helpers";
|
|
17
|
+
import { resolveOutboundMediaUrls } from "openclaw/plugin-sdk/reply-payload";
|
|
18
|
+
import { resolveSenderCommandAuthorization } from "openclaw/plugin-sdk/command-auth";
|
|
17
19
|
|
|
18
20
|
/* ── Inline helpers that may not exist in older OpenClaw versions ── */
|
|
19
21
|
|
|
@@ -30,7 +32,7 @@ function createScopedPairingAccess(params: {
|
|
|
30
32
|
channel: params.channel,
|
|
31
33
|
accountId: resolvedAccountId,
|
|
32
34
|
}),
|
|
33
|
-
upsertPairingRequest: (input: { id: string; meta?: Record<string,
|
|
35
|
+
upsertPairingRequest: (input: { id: string; meta?: Record<string, string | null | undefined> }) =>
|
|
34
36
|
params.core.channel.pairing.upsertPairingRequest({
|
|
35
37
|
channel: params.channel,
|
|
36
38
|
accountId: resolvedAccountId,
|
|
@@ -51,7 +53,7 @@ import {
|
|
|
51
53
|
resolveDefaultDchatAccountId,
|
|
52
54
|
} from "./config-schema.js";
|
|
53
55
|
import { NknBus } from "./nkn-bus.js";
|
|
54
|
-
import {
|
|
56
|
+
import { dchatSetupWizard } from "./onboarding.js";
|
|
55
57
|
import { getDchatRuntime } from "./runtime.js";
|
|
56
58
|
import { SeenTracker } from "./seen-tracker.js";
|
|
57
59
|
import type { ResolvedDchatAccount } from "./types.js";
|
|
@@ -92,7 +94,7 @@ function getBusForAccount(accountId: string): NknBus | undefined {
|
|
|
92
94
|
export const dchatPlugin: ChannelPlugin<ResolvedDchatAccount> = {
|
|
93
95
|
id: "dchat",
|
|
94
96
|
meta,
|
|
95
|
-
|
|
97
|
+
setupWizard: dchatSetupWizard,
|
|
96
98
|
capabilities: {
|
|
97
99
|
chatTypes: ["direct", "group"],
|
|
98
100
|
media: true,
|
|
@@ -138,7 +140,7 @@ export const dchatPlugin: ChannelPlugin<ResolvedDchatAccount> = {
|
|
|
138
140
|
name: account.name,
|
|
139
141
|
enabled: account.enabled,
|
|
140
142
|
configured: account.configured,
|
|
141
|
-
|
|
143
|
+
publicKey: account.nknAddress ?? null,
|
|
142
144
|
}),
|
|
143
145
|
resolveAllowFrom: ({ cfg, accountId }) => {
|
|
144
146
|
const dchatConfig = resolveDchatAccountConfig({ cfg: cfg as CoreConfig, accountId });
|
|
@@ -407,15 +409,15 @@ export const dchatPlugin: ChannelPlugin<ResolvedDchatAccount> = {
|
|
|
407
409
|
},
|
|
408
410
|
buildChannelSummary: ({ snapshot }) => ({
|
|
409
411
|
...buildBaseChannelStatusSummary(snapshot),
|
|
410
|
-
|
|
412
|
+
publicKey: snapshot.publicKey ?? null,
|
|
411
413
|
connected: snapshot.connected ?? false,
|
|
412
414
|
lastConnectedAt: snapshot.lastConnectedAt ?? null,
|
|
413
415
|
}),
|
|
414
416
|
buildAccountSnapshot: ({ account, runtime }) => ({
|
|
415
417
|
...buildBaseAccountStatusSnapshot({ account, runtime }),
|
|
416
|
-
|
|
417
|
-
connected: (runtime
|
|
418
|
-
lastConnectedAt:
|
|
418
|
+
publicKey: account.nknAddress ?? null,
|
|
419
|
+
connected: Boolean(runtime?.connected),
|
|
420
|
+
lastConnectedAt: runtime?.lastConnectedAt ?? null,
|
|
419
421
|
}),
|
|
420
422
|
collectStatusIssues: (accounts) => collectStatusIssuesFromLastError("dchat", accounts),
|
|
421
423
|
},
|
|
@@ -438,6 +440,39 @@ export const dchatPlugin: ChannelPlugin<ResolvedDchatAccount> = {
|
|
|
438
440
|
const seenTracker = new SeenTracker();
|
|
439
441
|
seenMap.set(account.accountId, seenTracker);
|
|
440
442
|
|
|
443
|
+
// Listen for heartbeat events
|
|
444
|
+
bus.on("heartbeat", ({ success, failures }: { success: boolean; failures: number }) => {
|
|
445
|
+
if (!success) {
|
|
446
|
+
logger.warn(`[${account.accountId}] heartbeat echo failed (${failures} consecutive)`);
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
bus.on("heartbeatReconnect", ({ failures }: { failures: number }) => {
|
|
450
|
+
logger.warn(
|
|
451
|
+
`[${account.accountId}] heartbeat failed ${failures} times, reconnecting...`,
|
|
452
|
+
);
|
|
453
|
+
ctx.setStatus({ accountId: account.accountId, connected: false });
|
|
454
|
+
});
|
|
455
|
+
let initialConnectDone = false;
|
|
456
|
+
bus.on("stateChange", (state: string) => {
|
|
457
|
+
if (state === "connected" && initialConnectDone) {
|
|
458
|
+
ctx.setStatus({
|
|
459
|
+
accountId: account.accountId,
|
|
460
|
+
connected: true,
|
|
461
|
+
lastConnectedAt: Date.now(),
|
|
462
|
+
});
|
|
463
|
+
logger.info(`[${account.accountId}] reconnected as ${bus.getAddress()}`);
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
bus.on("reconnectFailed", (err: unknown) => {
|
|
467
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
468
|
+
logger.error(`[${account.accountId}] reconnect failed: ${msg}`);
|
|
469
|
+
ctx.setStatus({
|
|
470
|
+
accountId: account.accountId,
|
|
471
|
+
connected: false,
|
|
472
|
+
lastError: `reconnect failed: ${msg}`,
|
|
473
|
+
});
|
|
474
|
+
});
|
|
475
|
+
|
|
441
476
|
try {
|
|
442
477
|
const address = await bus.connect(
|
|
443
478
|
{ seed: account.seed, numSubClients: account.numSubClients },
|
|
@@ -453,6 +488,7 @@ export const dchatPlugin: ChannelPlugin<ResolvedDchatAccount> = {
|
|
|
453
488
|
lastConnectedAt: Date.now(),
|
|
454
489
|
});
|
|
455
490
|
logger.info(`[${account.accountId}] connected as ${address}`);
|
|
491
|
+
initialConnectDone = true;
|
|
456
492
|
|
|
457
493
|
// Register inbound message handler
|
|
458
494
|
bus.onMessage((rawSrc, rawPayload) => {
|
package/src/config-schema.ts
CHANGED
package/src/nkn-bus.ts
CHANGED
|
@@ -5,12 +5,20 @@ import { NKN_SEED_RPC_SERVERS, type NknConnectionState } from "./types.js";
|
|
|
5
5
|
export interface NknBusOptions {
|
|
6
6
|
seed: string;
|
|
7
7
|
numSubClients?: number;
|
|
8
|
+
/** Heartbeat echo-test interval in ms (default 60 000). Set 0 to disable. */
|
|
9
|
+
heartbeatIntervalMs?: number;
|
|
10
|
+
/** Consecutive heartbeat failures before reconnecting (default 3). */
|
|
11
|
+
heartbeatMaxFailures?: number;
|
|
8
12
|
}
|
|
9
13
|
|
|
14
|
+
/** Payload used for heartbeat echo-test messages (self → self). */
|
|
15
|
+
const HEARTBEAT_ECHO_PREFIX = "__nkn_heartbeat_echo__:";
|
|
16
|
+
|
|
10
17
|
/**
|
|
11
18
|
* NKN MultiClient wrapper for D-Chat wire-format messaging.
|
|
12
|
-
* Handles connect, send, receive, subscribe, and reconnection.
|
|
19
|
+
* Handles connect, send, receive, subscribe, heartbeat, and reconnection.
|
|
13
20
|
*/
|
|
21
|
+
|
|
14
22
|
export class NknBus extends EventEmitter {
|
|
15
23
|
private client: nkn.MultiClient | null = null;
|
|
16
24
|
private state: NknConnectionState = "disconnected";
|
|
@@ -20,6 +28,14 @@ export class NknBus extends EventEmitter {
|
|
|
20
28
|
private numSubClients: number;
|
|
21
29
|
private abortSignal: AbortSignal | undefined;
|
|
22
30
|
|
|
31
|
+
private heartbeatIntervalMs: number = 60_000;
|
|
32
|
+
private heartbeatMaxFailures: number = 3;
|
|
33
|
+
private heartbeatTimer: ReturnType<typeof setTimeout> | null = null;
|
|
34
|
+
private heartbeatFailures: number = 0;
|
|
35
|
+
private pendingEchoId: string | null = null;
|
|
36
|
+
private pendingEchoResolve: (() => void) | null = null;
|
|
37
|
+
private isReconnecting: boolean = false;
|
|
38
|
+
|
|
23
39
|
constructor() {
|
|
24
40
|
super();
|
|
25
41
|
this.numSubClients = 4;
|
|
@@ -40,6 +56,8 @@ export class NknBus extends EventEmitter {
|
|
|
40
56
|
|
|
41
57
|
this.seed = opts.seed;
|
|
42
58
|
this.numSubClients = opts.numSubClients ?? 4;
|
|
59
|
+
this.heartbeatIntervalMs = opts.heartbeatIntervalMs ?? 60_000;
|
|
60
|
+
this.heartbeatMaxFailures = opts.heartbeatMaxFailures ?? 3;
|
|
43
61
|
this.abortSignal = abortSignal;
|
|
44
62
|
this.setState("connecting");
|
|
45
63
|
|
|
@@ -86,9 +104,23 @@ export class NknBus extends EventEmitter {
|
|
|
86
104
|
} else {
|
|
87
105
|
data = payload;
|
|
88
106
|
}
|
|
107
|
+
|
|
108
|
+
// Intercept heartbeat echo responses — don't emit as regular messages
|
|
109
|
+
if (data.startsWith(HEARTBEAT_ECHO_PREFIX)) {
|
|
110
|
+
const echoId = data.slice(HEARTBEAT_ECHO_PREFIX.length);
|
|
111
|
+
if (echoId === this.pendingEchoId && this.pendingEchoResolve) {
|
|
112
|
+
this.pendingEchoResolve();
|
|
113
|
+
this.pendingEchoResolve = null;
|
|
114
|
+
this.pendingEchoId = null;
|
|
115
|
+
}
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
89
119
|
this.emit("message", src, data);
|
|
90
120
|
});
|
|
91
121
|
|
|
122
|
+
this.startHeartbeat();
|
|
123
|
+
|
|
92
124
|
return this.address;
|
|
93
125
|
} catch (err) {
|
|
94
126
|
this.setState("disconnected");
|
|
@@ -105,6 +137,7 @@ export class NknBus extends EventEmitter {
|
|
|
105
137
|
}
|
|
106
138
|
|
|
107
139
|
async disconnect(): Promise<void> {
|
|
140
|
+
this.stopHeartbeat();
|
|
108
141
|
if (this.reconnectTimer) {
|
|
109
142
|
clearTimeout(this.reconnectTimer);
|
|
110
143
|
this.reconnectTimer = null;
|
|
@@ -200,6 +233,185 @@ export class NknBus extends EventEmitter {
|
|
|
200
233
|
this.on("message", handler);
|
|
201
234
|
}
|
|
202
235
|
|
|
236
|
+
/** Start periodic heartbeat echo test (self → self). */
|
|
237
|
+
private startHeartbeat(): void {
|
|
238
|
+
if (this.heartbeatIntervalMs <= 0) return;
|
|
239
|
+
this.heartbeatFailures = 0;
|
|
240
|
+
this.scheduleNextHeartbeat();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
private stopHeartbeat(): void {
|
|
244
|
+
if (this.heartbeatTimer) {
|
|
245
|
+
clearTimeout(this.heartbeatTimer);
|
|
246
|
+
this.heartbeatTimer = null;
|
|
247
|
+
}
|
|
248
|
+
// Reject any pending echo wait
|
|
249
|
+
this.pendingEchoResolve = null;
|
|
250
|
+
this.pendingEchoId = null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private scheduleNextHeartbeat(): void {
|
|
254
|
+
if (this.heartbeatTimer) {
|
|
255
|
+
clearTimeout(this.heartbeatTimer);
|
|
256
|
+
}
|
|
257
|
+
this.heartbeatTimer = setTimeout(() => {
|
|
258
|
+
void this.runHeartbeat();
|
|
259
|
+
}, this.heartbeatIntervalMs);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
private async runHeartbeat(): Promise<void> {
|
|
263
|
+
if (!this.client || this.state !== "connected" || !this.address) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const echoId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
268
|
+
const echoPayload = HEARTBEAT_ECHO_PREFIX + echoId;
|
|
269
|
+
|
|
270
|
+
try {
|
|
271
|
+
const echoReceived = await new Promise<boolean>((resolve) => {
|
|
272
|
+
this.pendingEchoId = echoId;
|
|
273
|
+
// Timeout: half the heartbeat interval or 15s, whichever is smaller
|
|
274
|
+
const timeout = Math.min(this.heartbeatIntervalMs / 2, 15_000);
|
|
275
|
+
const timer = setTimeout(() => {
|
|
276
|
+
this.pendingEchoResolve = null;
|
|
277
|
+
this.pendingEchoId = null;
|
|
278
|
+
resolve(false);
|
|
279
|
+
}, timeout);
|
|
280
|
+
|
|
281
|
+
this.pendingEchoResolve = () => {
|
|
282
|
+
clearTimeout(timer);
|
|
283
|
+
resolve(true);
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
// Send echo to self (fire-and-forget send, we wait for the message handler)
|
|
287
|
+
this.client!.send(this.address!, echoPayload, { noReply: true });
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
if (echoReceived) {
|
|
291
|
+
this.heartbeatFailures = 0;
|
|
292
|
+
this.emit("heartbeat", { success: true, failures: 0 });
|
|
293
|
+
} else {
|
|
294
|
+
this.heartbeatFailures++;
|
|
295
|
+
this.emit("heartbeat", { success: false, failures: this.heartbeatFailures });
|
|
296
|
+
|
|
297
|
+
if (this.heartbeatFailures >= this.heartbeatMaxFailures) {
|
|
298
|
+
this.emit("heartbeatReconnect", { failures: this.heartbeatFailures });
|
|
299
|
+
await this.reconnect();
|
|
300
|
+
return; // reconnect starts a new heartbeat loop
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
} catch {
|
|
304
|
+
this.heartbeatFailures++;
|
|
305
|
+
this.emit("heartbeat", { success: false, failures: this.heartbeatFailures });
|
|
306
|
+
|
|
307
|
+
if (this.heartbeatFailures >= this.heartbeatMaxFailures) {
|
|
308
|
+
this.emit("heartbeatReconnect", { failures: this.heartbeatFailures });
|
|
309
|
+
await this.reconnect();
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Schedule next heartbeat if still connected
|
|
315
|
+
if (this.state === "connected") {
|
|
316
|
+
this.scheduleNextHeartbeat();
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Close the current connection and create a new one using stored options. */
|
|
321
|
+
private async reconnect(): Promise<void> {
|
|
322
|
+
if (this.isReconnecting || !this.seed) return;
|
|
323
|
+
this.isReconnecting = true;
|
|
324
|
+
|
|
325
|
+
try {
|
|
326
|
+
this.stopHeartbeat();
|
|
327
|
+
|
|
328
|
+
// Close existing client
|
|
329
|
+
if (this.client) {
|
|
330
|
+
try {
|
|
331
|
+
this.client.close();
|
|
332
|
+
} catch {
|
|
333
|
+
// ignore close errors
|
|
334
|
+
}
|
|
335
|
+
this.client = null;
|
|
336
|
+
}
|
|
337
|
+
this.address = undefined;
|
|
338
|
+
this.setState("connecting");
|
|
339
|
+
|
|
340
|
+
// Create a fresh client
|
|
341
|
+
this.client = new nkn.MultiClient({
|
|
342
|
+
seed: this.seed,
|
|
343
|
+
numSubClients: this.numSubClients,
|
|
344
|
+
originalClient: false,
|
|
345
|
+
rpcServerAddr: NKN_SEED_RPC_SERVERS[0],
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
await new Promise<void>((resolve, reject) => {
|
|
349
|
+
const timeout = setTimeout(() => {
|
|
350
|
+
reject(new Error("NKN reconnection timeout after 30s"));
|
|
351
|
+
}, 30_000);
|
|
352
|
+
|
|
353
|
+
if (this.abortSignal?.aborted) {
|
|
354
|
+
clearTimeout(timeout);
|
|
355
|
+
reject(new Error("Aborted"));
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const onAbort = () => {
|
|
360
|
+
clearTimeout(timeout);
|
|
361
|
+
reject(new Error("Aborted"));
|
|
362
|
+
};
|
|
363
|
+
this.abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
364
|
+
|
|
365
|
+
this.client!.onConnect(() => {
|
|
366
|
+
clearTimeout(timeout);
|
|
367
|
+
this.abortSignal?.removeEventListener("abort", onAbort);
|
|
368
|
+
resolve();
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
this.address = this.client.addr;
|
|
373
|
+
this.setState("connected");
|
|
374
|
+
|
|
375
|
+
// Re-register message handler
|
|
376
|
+
this.client.onMessage(({ src, payload }: { src: string; payload: Uint8Array | string }) => {
|
|
377
|
+
let data: string;
|
|
378
|
+
if (payload instanceof Uint8Array) {
|
|
379
|
+
data = new TextDecoder().decode(payload);
|
|
380
|
+
} else {
|
|
381
|
+
data = payload;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (data.startsWith(HEARTBEAT_ECHO_PREFIX)) {
|
|
385
|
+
const id = data.slice(HEARTBEAT_ECHO_PREFIX.length);
|
|
386
|
+
if (id === this.pendingEchoId && this.pendingEchoResolve) {
|
|
387
|
+
this.pendingEchoResolve();
|
|
388
|
+
this.pendingEchoResolve = null;
|
|
389
|
+
this.pendingEchoId = null;
|
|
390
|
+
}
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
this.emit("message", src, data);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
this.heartbeatFailures = 0;
|
|
398
|
+
this.startHeartbeat();
|
|
399
|
+
} catch (err) {
|
|
400
|
+
this.setState("disconnected");
|
|
401
|
+
if (this.client) {
|
|
402
|
+
try {
|
|
403
|
+
this.client.close();
|
|
404
|
+
} catch {
|
|
405
|
+
// ignore
|
|
406
|
+
}
|
|
407
|
+
this.client = null;
|
|
408
|
+
}
|
|
409
|
+
this.emit("reconnectFailed", err);
|
|
410
|
+
} finally {
|
|
411
|
+
this.isReconnecting = false;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
203
415
|
private ensureConnected(): void {
|
|
204
416
|
if (!this.client || this.state !== "connected") {
|
|
205
417
|
throw new Error("NKN client not connected");
|
package/src/onboarding.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import type { DmPolicy } from "openclaw/plugin-sdk";
|
|
1
|
+
import type { DmPolicy } from "openclaw/plugin-sdk/setup";
|
|
2
2
|
import {
|
|
3
3
|
addWildcardAllowFrom,
|
|
4
4
|
mergeAllowFromEntries,
|
|
5
5
|
formatDocsLink,
|
|
6
|
-
type
|
|
7
|
-
type ChannelOnboardingDmPolicy,
|
|
6
|
+
type ChannelSetupDmPolicy,
|
|
8
7
|
type WizardPrompter,
|
|
9
|
-
} from "openclaw/plugin-sdk";
|
|
10
|
-
import {
|
|
8
|
+
} from "openclaw/plugin-sdk/setup";
|
|
9
|
+
import type { ChannelSetupWizard } from "openclaw/plugin-sdk/setup";
|
|
10
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
|
|
11
|
+
import { listDchatAccountIds, resolveDchatAccount, resolveDchatAccountConfig, type CoreConfig } from "./config-schema.js";
|
|
11
12
|
|
|
12
13
|
const channel = "dchat" as const;
|
|
13
14
|
|
|
@@ -30,154 +31,134 @@ function setDchatDmPolicy(cfg: CoreConfig, policy: DmPolicy) {
|
|
|
30
31
|
};
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
cfg: CoreConfig;
|
|
35
|
-
prompter: WizardPrompter;
|
|
36
|
-
}): Promise<CoreConfig> {
|
|
37
|
-
const { cfg, prompter } = params;
|
|
38
|
-
const existingAllowFrom = cfg.channels?.dchat?.allowFrom ?? [];
|
|
39
|
-
|
|
40
|
-
const entry = await prompter.text({
|
|
41
|
-
message: "NKN address to allow (full public key hex)",
|
|
42
|
-
placeholder: "abc123...def456 (64-char hex NKN address)",
|
|
43
|
-
initialValue: existingAllowFrom[0] ? String(existingAllowFrom[0]) : undefined,
|
|
44
|
-
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
const parts = String(entry)
|
|
48
|
-
.split(/[\n,;]+/g)
|
|
49
|
-
.map((e) => e.trim())
|
|
50
|
-
.filter(Boolean);
|
|
51
|
-
|
|
52
|
-
const unique = mergeAllowFromEntries(existingAllowFrom, parts);
|
|
53
|
-
return {
|
|
54
|
-
...cfg,
|
|
55
|
-
channels: {
|
|
56
|
-
...cfg.channels,
|
|
57
|
-
dchat: {
|
|
58
|
-
...cfg.channels?.dchat,
|
|
59
|
-
enabled: true,
|
|
60
|
-
dmPolicy: "allowlist",
|
|
61
|
-
allowFrom: unique,
|
|
62
|
-
},
|
|
63
|
-
},
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const dmPolicy: ChannelOnboardingDmPolicy = {
|
|
34
|
+
const dmPolicy: ChannelSetupDmPolicy = {
|
|
68
35
|
label: "D-Chat",
|
|
69
36
|
channel,
|
|
70
37
|
policyKey: "channels.dchat.dmPolicy",
|
|
71
38
|
allowFromKey: "channels.dchat.allowFrom",
|
|
72
39
|
getCurrent: (cfg) => ((cfg as CoreConfig).channels?.dchat?.dmPolicy as DmPolicy) ?? "pairing",
|
|
73
40
|
setPolicy: (cfg, policy) => setDchatDmPolicy(cfg as CoreConfig, policy),
|
|
74
|
-
promptAllowFrom:
|
|
75
|
-
|
|
41
|
+
promptAllowFrom: async (params) => {
|
|
42
|
+
const cfg = params.cfg as CoreConfig;
|
|
43
|
+
const { prompter } = params;
|
|
44
|
+
const existingAllowFrom = cfg.channels?.dchat?.allowFrom ?? [];
|
|
76
45
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
)
|
|
46
|
+
const entry = await prompter.text({
|
|
47
|
+
message: "NKN address to allow (full public key hex)",
|
|
48
|
+
placeholder: "abc123...def456 (64-char hex NKN address)",
|
|
49
|
+
initialValue: existingAllowFrom[0] ? String(existingAllowFrom[0]) : undefined,
|
|
50
|
+
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const parts = String(entry)
|
|
54
|
+
.split(/[\n,;]+/g)
|
|
55
|
+
.map((e) => e.trim())
|
|
56
|
+
.filter(Boolean);
|
|
57
|
+
|
|
58
|
+
const unique = mergeAllowFromEntries(existingAllowFrom, parts);
|
|
85
59
|
return {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
60
|
+
...cfg,
|
|
61
|
+
channels: {
|
|
62
|
+
...cfg.channels,
|
|
63
|
+
dchat: {
|
|
64
|
+
...cfg.channels?.dchat,
|
|
65
|
+
enabled: true,
|
|
66
|
+
dmPolicy: "allowlist",
|
|
67
|
+
allowFrom: unique,
|
|
68
|
+
},
|
|
69
|
+
},
|
|
90
70
|
};
|
|
91
71
|
},
|
|
92
|
-
|
|
93
|
-
let next = cfg as CoreConfig;
|
|
94
|
-
const existing = next.channels?.dchat ?? {};
|
|
95
|
-
const account = resolveDchatAccount({ cfg: next });
|
|
72
|
+
};
|
|
96
73
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
74
|
+
export const dchatSetupWizard: ChannelSetupWizard = {
|
|
75
|
+
channel,
|
|
76
|
+
status: {
|
|
77
|
+
configuredLabel: "configured",
|
|
78
|
+
unconfiguredLabel: "needs wallet seed",
|
|
79
|
+
resolveConfigured: ({ cfg }) => {
|
|
80
|
+
const typedCfg = cfg as CoreConfig;
|
|
81
|
+
const accountIds = listDchatAccountIds(typedCfg);
|
|
82
|
+
return accountIds.some(
|
|
83
|
+
(id) => resolveDchatAccount({ cfg: typedCfg, accountId: id }).configured,
|
|
106
84
|
);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
85
|
+
},
|
|
86
|
+
resolveStatusLines: ({ cfg, configured }) => [
|
|
87
|
+
`D-Chat: ${configured ? "configured" : "needs wallet seed"}`,
|
|
88
|
+
],
|
|
89
|
+
resolveSelectionHint: ({ configured }) =>
|
|
90
|
+
configured ? "configured" : "needs seed",
|
|
91
|
+
},
|
|
92
|
+
introNote: {
|
|
93
|
+
title: "D-Chat setup",
|
|
94
|
+
lines: [
|
|
95
|
+
"D-Chat uses the NKN relay network for decentralized E2E encrypted messaging.",
|
|
96
|
+
"You need a wallet seed (64-character hex string) to connect.",
|
|
97
|
+
"Generate one with nkn-sdk or use an existing seed from D-Chat/nMobile.",
|
|
98
|
+
`Docs: ${formatDocsLink("/channels/dchat", "channels/dchat")}`,
|
|
99
|
+
],
|
|
100
|
+
shouldShow: ({ cfg }) => {
|
|
101
|
+
const account = resolveDchatAccount({ cfg: cfg as CoreConfig });
|
|
102
|
+
return !account.configured;
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
envShortcut: {
|
|
106
|
+
prompt: "NKN seed env var detected. Use env value?",
|
|
107
|
+
preferredEnvVar: "DCHAT_SEED",
|
|
108
|
+
isAvailable: ({ cfg }) => {
|
|
109
|
+
const envSeed = process.env.DCHAT_SEED?.trim() || process.env.NKN_SEED?.trim();
|
|
110
|
+
const existing = (cfg as CoreConfig).channels?.dchat?.seed;
|
|
111
|
+
return Boolean(envSeed && /^[0-9a-f]{64}$/i.test(envSeed) && !existing);
|
|
112
|
+
},
|
|
113
|
+
apply: ({ cfg, accountId }) => {
|
|
114
|
+
const envSeed = (process.env.DCHAT_SEED?.trim() || process.env.NKN_SEED?.trim())!;
|
|
115
|
+
return {
|
|
116
|
+
...cfg,
|
|
117
|
+
channels: {
|
|
118
|
+
...(cfg as CoreConfig).channels,
|
|
119
|
+
dchat: {
|
|
120
|
+
...(cfg as CoreConfig).channels?.dchat,
|
|
121
|
+
enabled: true,
|
|
122
|
+
seed: envSeed,
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
} as OpenClawConfig;
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
credentials: [
|
|
129
|
+
{
|
|
130
|
+
inputKey: "accessToken",
|
|
131
|
+
providerHint: "NKN",
|
|
132
|
+
credentialLabel: "wallet seed",
|
|
133
|
+
preferredEnvVar: "DCHAT_SEED",
|
|
134
|
+
envPrompt: "NKN seed env var detected. Use env value?",
|
|
135
|
+
keepPrompt: "Wallet seed already configured. Keep it?",
|
|
136
|
+
inputPrompt: "NKN wallet seed (64-char hex)",
|
|
137
|
+
inspect: ({ cfg, accountId }) => {
|
|
138
|
+
const acctCfg = resolveDchatAccountConfig({ cfg: cfg as CoreConfig, accountId });
|
|
139
|
+
const seed = acctCfg.seed?.trim();
|
|
140
|
+
return {
|
|
141
|
+
accountConfigured: Boolean(seed),
|
|
142
|
+
hasConfiguredValue: Boolean(seed),
|
|
143
|
+
resolvedValue: seed,
|
|
144
|
+
};
|
|
145
|
+
},
|
|
146
|
+
applySet: ({ cfg, accountId, resolvedValue }) => {
|
|
147
|
+
const seed = resolvedValue.trim();
|
|
148
|
+
return {
|
|
149
|
+
...cfg,
|
|
119
150
|
channels: {
|
|
120
|
-
...
|
|
151
|
+
...(cfg as CoreConfig).channels,
|
|
121
152
|
dchat: {
|
|
122
|
-
...
|
|
153
|
+
...(cfg as CoreConfig).channels?.dchat,
|
|
123
154
|
enabled: true,
|
|
124
|
-
seed
|
|
155
|
+
seed,
|
|
125
156
|
},
|
|
126
157
|
},
|
|
127
|
-
};
|
|
128
|
-
if (forceAllowFrom) {
|
|
129
|
-
next = await promptDchatAllowFrom({ cfg: next, prompter });
|
|
130
|
-
}
|
|
131
|
-
return { cfg: next };
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Prompt for seed
|
|
136
|
-
let seed = existing.seed ?? "";
|
|
137
|
-
if (seed) {
|
|
138
|
-
const keep = await prompter.confirm({
|
|
139
|
-
message: "Wallet seed already configured. Keep it?",
|
|
140
|
-
initialValue: true,
|
|
141
|
-
});
|
|
142
|
-
if (!keep) {
|
|
143
|
-
seed = "";
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
if (!seed) {
|
|
148
|
-
seed = String(
|
|
149
|
-
await prompter.text({
|
|
150
|
-
message: "NKN wallet seed (64-char hex)",
|
|
151
|
-
validate: (value) => {
|
|
152
|
-
const raw = String(value ?? "").trim();
|
|
153
|
-
if (!raw) return "Required";
|
|
154
|
-
if (!/^[0-9a-f]{64}$/i.test(raw)) {
|
|
155
|
-
return "Must be a 64-character hex string";
|
|
156
|
-
}
|
|
157
|
-
return undefined;
|
|
158
|
-
},
|
|
159
|
-
}),
|
|
160
|
-
).trim();
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
next = {
|
|
164
|
-
...next,
|
|
165
|
-
channels: {
|
|
166
|
-
...next.channels,
|
|
167
|
-
dchat: {
|
|
168
|
-
...next.channels?.dchat,
|
|
169
|
-
enabled: true,
|
|
170
|
-
seed,
|
|
171
|
-
},
|
|
158
|
+
} as OpenClawConfig;
|
|
172
159
|
},
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
if (forceAllowFrom) {
|
|
176
|
-
next = await promptDchatAllowFrom({ cfg: next, prompter });
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
return { cfg: next };
|
|
180
|
-
},
|
|
160
|
+
},
|
|
161
|
+
],
|
|
181
162
|
dmPolicy,
|
|
182
163
|
disable: (cfg) => ({
|
|
183
164
|
...(cfg as CoreConfig),
|
package/src/runtime.ts
CHANGED