@unicitylabs/openclaw-unicity 0.3.0 → 0.3.1
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 +84 -15
- package/src/tools/create-private-group.ts +4 -2
package/package.json
CHANGED
package/src/channel.ts
CHANGED
|
@@ -10,6 +10,24 @@ import type { UnicityConfig } from "./config.js";
|
|
|
10
10
|
|
|
11
11
|
const DEFAULT_ACCOUNT_ID = "default";
|
|
12
12
|
|
|
13
|
+
/** How long (ms) to wait after the last group message before declaring backfill complete. */
|
|
14
|
+
export const GROUP_BACKFILL_DEBOUNCE_MS = 3_000;
|
|
15
|
+
|
|
16
|
+
interface GroupBackfillState {
|
|
17
|
+
phase: "buffering" | "live";
|
|
18
|
+
latestMsg: {
|
|
19
|
+
id?: string;
|
|
20
|
+
groupId: string;
|
|
21
|
+
senderPubkey: string;
|
|
22
|
+
senderNametag?: string;
|
|
23
|
+
content: string;
|
|
24
|
+
timestamp: number;
|
|
25
|
+
replyToId?: string;
|
|
26
|
+
} | null;
|
|
27
|
+
bufferedCount: number;
|
|
28
|
+
timer: ReturnType<typeof setTimeout> | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
13
31
|
// ---------------------------------------------------------------------------
|
|
14
32
|
// Account config shape (read from openclaw config under channels.unicity)
|
|
15
33
|
// ---------------------------------------------------------------------------
|
|
@@ -422,21 +440,22 @@ export const unicityChannelPlugin = {
|
|
|
422
440
|
});
|
|
423
441
|
});
|
|
424
442
|
|
|
425
|
-
//
|
|
426
|
-
|
|
427
|
-
|
|
443
|
+
// -- Group message dispatch helper & backfill debounce --------------------
|
|
444
|
+
|
|
445
|
+
type GroupMsg = {
|
|
446
|
+
id?: string;
|
|
428
447
|
groupId: string;
|
|
429
|
-
groupName?: string;
|
|
430
448
|
senderPubkey: string;
|
|
431
449
|
senderNametag?: string;
|
|
432
450
|
content: string;
|
|
433
|
-
timestamp
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
if (msg.senderPubkey === sphere.identity?.chainPubkey) return;
|
|
451
|
+
timestamp: number;
|
|
452
|
+
replyToId?: string;
|
|
453
|
+
};
|
|
437
454
|
|
|
455
|
+
function dispatchGroupMessage(msg: GroupMsg): void {
|
|
438
456
|
const senderName = msg.senderNametag ?? msg.senderPubkey.slice(0, 12);
|
|
439
|
-
const
|
|
457
|
+
const groupData = sphere.groupChat?.getGroup?.(msg.groupId);
|
|
458
|
+
const groupName = groupData?.name ?? msg.groupId;
|
|
440
459
|
const isOwner = isSenderOwner(msg.senderPubkey, msg.senderNametag);
|
|
441
460
|
const metadataHeader = `[SenderName: ${senderName} | SenderId: ${msg.senderPubkey} | GroupId: ${msg.groupId} | GroupName: ${groupName} | IsOwner: ${isOwner} | CommandAuthorized: ${isOwner}]`;
|
|
442
461
|
const sanitizedContent = msg.content.replace(/\[(?:SenderName|SenderId|IsOwner|CommandAuthorized|GroupId|GroupName)\s*:/gi, "[BLOCKED:");
|
|
@@ -487,40 +506,89 @@ export const unicityChannelPlugin = {
|
|
|
487
506
|
.catch((err: unknown) => {
|
|
488
507
|
ctx.log?.error(`[${ctx.account.accountId}] Group message dispatch error: ${err}`);
|
|
489
508
|
});
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Per-group backfill state: buffer messages during the initial burst, then
|
|
512
|
+
// switch to live dispatch once the burst settles.
|
|
513
|
+
const groupBackfillStates = new Map<string, GroupBackfillState>();
|
|
514
|
+
|
|
515
|
+
// Subscribe to incoming group messages
|
|
516
|
+
const unsubGroupMessage = sphere.groupChat?.onMessage?.((msg: GroupMsg) => {
|
|
517
|
+
// Skip messages from self
|
|
518
|
+
if (msg.senderPubkey === sphere.identity?.chainPubkey) return;
|
|
519
|
+
|
|
520
|
+
// Lookup or create per-group backfill state
|
|
521
|
+
let state = groupBackfillStates.get(msg.groupId);
|
|
522
|
+
if (!state) {
|
|
523
|
+
state = { phase: "buffering", latestMsg: null, bufferedCount: 0, timer: null };
|
|
524
|
+
groupBackfillStates.set(msg.groupId, state);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Already past backfill — dispatch immediately
|
|
528
|
+
if (state.phase === "live") {
|
|
529
|
+
dispatchGroupMessage(msg);
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// BUFFERING: keep only the latest message, reset the debounce timer
|
|
534
|
+
state.latestMsg = msg;
|
|
535
|
+
state.bufferedCount++;
|
|
536
|
+
if (state.timer) clearTimeout(state.timer);
|
|
537
|
+
state.timer = setTimeout(() => {
|
|
538
|
+
state!.phase = "live";
|
|
539
|
+
state!.timer = null;
|
|
540
|
+
ctx.log?.info(
|
|
541
|
+
`[${ctx.account.accountId}] Group backfill settled for ${msg.groupId}, ${state!.bufferedCount} message(s) buffered`,
|
|
542
|
+
);
|
|
543
|
+
// Dispatch the most recent buffered message so the agent has context
|
|
544
|
+
if (state!.latestMsg) {
|
|
545
|
+
dispatchGroupMessage(state!.latestMsg);
|
|
546
|
+
state!.latestMsg = null;
|
|
547
|
+
}
|
|
548
|
+
}, GROUP_BACKFILL_DEBOUNCE_MS);
|
|
490
549
|
}) ?? (() => {});
|
|
491
550
|
|
|
492
551
|
// Subscribe to group lifecycle events and notify owner
|
|
493
|
-
const unsubGroupJoined = sphere.on?.("groupchat:joined", (event: {
|
|
552
|
+
const unsubGroupJoined = sphere.on?.("groupchat:joined", (event: { groupId: string; groupName: string }) => {
|
|
494
553
|
const owner = getOwnerIdentity();
|
|
495
554
|
if (owner) {
|
|
496
|
-
const label = event.
|
|
555
|
+
const label = event.groupName ? `${event.groupName} (${event.groupId})` : event.groupId;
|
|
497
556
|
sphere.communications.sendDM(`@${owner}`, `I joined group ${label}`).catch((err) => {
|
|
498
557
|
ctx.log?.error(`[${ctx.account.accountId}] Failed to notify owner about group join: ${err}`);
|
|
499
558
|
});
|
|
500
559
|
}
|
|
501
560
|
}) ?? (() => {});
|
|
502
561
|
|
|
503
|
-
const unsubGroupLeft = sphere.on?.("groupchat:left", (event: {
|
|
562
|
+
const unsubGroupLeft = sphere.on?.("groupchat:left", (event: { groupId: string }) => {
|
|
504
563
|
const owner = getOwnerIdentity();
|
|
505
564
|
if (owner) {
|
|
506
|
-
const
|
|
565
|
+
const groupData = sphere.groupChat?.getGroup?.(event.groupId);
|
|
566
|
+
const label = groupData?.name ?? event.groupId;
|
|
507
567
|
sphere.communications.sendDM(`@${owner}`, `I left group ${label}`).catch((err) => {
|
|
508
568
|
ctx.log?.error(`[${ctx.account.accountId}] Failed to notify owner about group leave: ${err}`);
|
|
509
569
|
});
|
|
510
570
|
}
|
|
511
571
|
}) ?? (() => {});
|
|
512
572
|
|
|
513
|
-
const unsubGroupKicked = sphere.on?.("groupchat:kicked", (event: {
|
|
573
|
+
const unsubGroupKicked = sphere.on?.("groupchat:kicked", (event: { groupId: string; groupName: string }) => {
|
|
514
574
|
const owner = getOwnerIdentity();
|
|
515
575
|
if (owner) {
|
|
516
|
-
const label = event.
|
|
576
|
+
const label = event.groupName ? `${event.groupName} (${event.groupId})` : event.groupId;
|
|
517
577
|
sphere.communications.sendDM(`@${owner}`, `I was kicked from group ${label}`).catch((err) => {
|
|
518
578
|
ctx.log?.error(`[${ctx.account.accountId}] Failed to notify owner about group kick: ${err}`);
|
|
519
579
|
});
|
|
520
580
|
}
|
|
521
581
|
}) ?? (() => {});
|
|
522
582
|
|
|
583
|
+
function clearBackfillTimers(): void {
|
|
584
|
+
for (const state of groupBackfillStates.values()) {
|
|
585
|
+
if (state.timer) clearTimeout(state.timer);
|
|
586
|
+
}
|
|
587
|
+
groupBackfillStates.clear();
|
|
588
|
+
}
|
|
589
|
+
|
|
523
590
|
ctx.abortSignal.addEventListener("abort", () => {
|
|
591
|
+
clearBackfillTimers();
|
|
524
592
|
unsub();
|
|
525
593
|
unsubTransfer();
|
|
526
594
|
unsubPaymentRequest();
|
|
@@ -532,6 +600,7 @@ export const unicityChannelPlugin = {
|
|
|
532
600
|
|
|
533
601
|
return {
|
|
534
602
|
stop: () => {
|
|
603
|
+
clearBackfillTimers();
|
|
535
604
|
unsub();
|
|
536
605
|
unsubTransfer();
|
|
537
606
|
unsubPaymentRequest();
|
|
@@ -28,8 +28,10 @@ export const createPrivateGroupTool = {
|
|
|
28
28
|
visibility: "private",
|
|
29
29
|
});
|
|
30
30
|
|
|
31
|
-
const
|
|
32
|
-
|
|
31
|
+
const joinCode = await sphere.groupChat.createInvite(group.id);
|
|
32
|
+
if (!joinCode) {
|
|
33
|
+
throw new Error("Failed to generate invite code for the private group");
|
|
34
|
+
}
|
|
33
35
|
|
|
34
36
|
const lines = [
|
|
35
37
|
`Private group created: ${group.name} (${group.id})`,
|