@newbase-clawchat/openclaw-clawchat 2026.4.29 → 2026.5.4
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 +37 -11
- package/dist/index.js +27 -0
- package/dist/src/api-client.js +156 -0
- package/dist/src/api-types.js +17 -0
- package/dist/src/buffered-stream.js +177 -0
- package/dist/src/channel.js +200 -0
- package/dist/src/client.js +176 -0
- package/dist/src/commands.js +35 -0
- package/dist/src/config.js +226 -0
- package/dist/src/inbound.js +133 -0
- package/dist/src/login.runtime.js +132 -0
- package/dist/src/media-runtime.js +85 -0
- package/dist/src/message-mapper.js +82 -0
- package/dist/src/outbound.js +181 -0
- package/dist/src/protocol.js +38 -0
- package/dist/src/reply-dispatcher.js +440 -0
- package/dist/src/runtime.js +288 -0
- package/dist/src/streaming.js +65 -0
- package/dist/src/tools-schema.js +38 -0
- package/dist/src/tools.js +287 -0
- package/openclaw.plugin.json +21 -0
- package/package.json +27 -5
- package/skills/clawchat-activate/SKILL.md +18 -9
- package/src/buffered-stream.test.ts +10 -0
- package/src/buffered-stream.ts +6 -6
- package/src/channel.outbound.test.ts +3 -3
- package/src/channel.test.ts +7 -1
- package/src/channel.ts +27 -8
- package/src/client.test.ts +8 -1
- package/src/client.ts +11 -10
- package/src/commands.test.ts +6 -0
- package/src/commands.ts +5 -1
- package/src/config.test.ts +47 -0
- package/src/config.ts +28 -5
- package/src/inbound.test.ts +4 -1
- package/src/inbound.ts +11 -10
- package/src/login.runtime.test.ts +36 -0
- package/src/login.runtime.ts +57 -27
- package/src/manifest.test.ts +156 -30
- package/src/outbound.test.ts +6 -5
- package/src/outbound.ts +8 -7
- package/src/plugin-entry.test.ts +7 -1
- package/src/reply-dispatcher.test.ts +418 -3
- package/src/reply-dispatcher.ts +137 -12
- package/src/runtime.ts +1 -0
- package/src/streaming.test.ts +12 -9
- package/src/streaming.ts +6 -6
- package/src/tools.test.ts +81 -18
- package/src/tools.ts +65 -74
package/src/outbound.ts
CHANGED
|
@@ -22,6 +22,7 @@ export interface OutboundTarget {
|
|
|
22
22
|
|
|
23
23
|
export interface OutboundReplyCtx {
|
|
24
24
|
replyToMessageId: string;
|
|
25
|
+
replyPreviewChatId?: string;
|
|
25
26
|
replyPreviewSenderId: string;
|
|
26
27
|
replyPreviewNickName: string;
|
|
27
28
|
replyPreviewText: string;
|
|
@@ -38,6 +39,7 @@ export interface SendParams {
|
|
|
38
39
|
to: OutboundTarget;
|
|
39
40
|
text: string;
|
|
40
41
|
replyCtx?: OutboundReplyCtx;
|
|
42
|
+
richFragments?: Fragment[];
|
|
41
43
|
mediaFragments?: ClawlingMediaFragment[];
|
|
42
44
|
mentions?: string[];
|
|
43
45
|
log?: LogSink;
|
|
@@ -92,8 +94,9 @@ export function parseOpenclawRecipient(to: string): { chatId: string; chatType:
|
|
|
92
94
|
|
|
93
95
|
export async function sendOpenclawClawlingText(params: SendParams): Promise<SendResult | null> {
|
|
94
96
|
const text = (params.text ?? "").trim();
|
|
97
|
+
const richFragments = params.richFragments ?? [];
|
|
95
98
|
const mediaFragments = params.mediaFragments ?? [];
|
|
96
|
-
if (!text && mediaFragments.length === 0) {
|
|
99
|
+
if (!text && richFragments.length === 0 && mediaFragments.length === 0) {
|
|
97
100
|
params.log?.info?.(
|
|
98
101
|
`[${params.account.accountId}] openclaw-clawchat outbound suppressed: empty text and no media`,
|
|
99
102
|
);
|
|
@@ -106,7 +109,7 @@ export async function sendOpenclawClawlingText(params: SendParams): Promise<Send
|
|
|
106
109
|
// with one of the SDK's narrow Fragment members (ImageFragment / FileFragment /
|
|
107
110
|
// AudioFragment / VideoFragment) based on its runtime `kind`. The wide local
|
|
108
111
|
// shape lets us build a single uniform array without a per-kind switch.
|
|
109
|
-
const fragments = [...textFragments, ...mediaFragments] as Fragment[];
|
|
112
|
+
const fragments = [...textFragments, ...richFragments, ...mediaFragments] as Fragment[];
|
|
110
113
|
|
|
111
114
|
const useReply = params.replyCtx && mediaFragments.length === 0;
|
|
112
115
|
if (params.replyCtx && mediaFragments.length > 0) {
|
|
@@ -121,26 +124,24 @@ export async function sendOpenclawClawlingText(params: SendParams): Promise<Send
|
|
|
121
124
|
mode = "reply";
|
|
122
125
|
ack = await params.client.replyMessage({
|
|
123
126
|
chat_id: params.to.chatId,
|
|
124
|
-
chat_type: params.to.chatType,
|
|
125
127
|
mode: "normal",
|
|
126
128
|
replyTo: {
|
|
127
129
|
msgId: params.replyCtx.replyToMessageId,
|
|
128
|
-
senderId: params.replyCtx.replyPreviewSenderId,
|
|
130
|
+
senderId: params.replyCtx.replyPreviewChatId ?? params.replyCtx.replyPreviewSenderId,
|
|
129
131
|
nickName: params.replyCtx.replyPreviewNickName,
|
|
130
132
|
fragments: [{ kind: "text", text: params.replyCtx.replyPreviewText }],
|
|
131
133
|
},
|
|
132
134
|
body: { fragments },
|
|
133
135
|
context: { mentions },
|
|
134
|
-
});
|
|
136
|
+
} as Parameters<ClawlingChatClient["replyMessage"]>[0]);
|
|
135
137
|
} else {
|
|
136
138
|
mode = "send";
|
|
137
139
|
ack = await params.client.sendMessage({
|
|
138
140
|
chat_id: params.to.chatId,
|
|
139
|
-
chat_type: params.to.chatType,
|
|
140
141
|
mode: "normal",
|
|
141
142
|
body: { fragments },
|
|
142
143
|
context: { mentions, reply: null },
|
|
143
|
-
});
|
|
144
|
+
} as Parameters<ClawlingChatClient["sendMessage"]>[0]);
|
|
144
145
|
}
|
|
145
146
|
params.log?.info?.(
|
|
146
147
|
`[${params.account.accountId}] openclaw-clawchat outbound mode=${mode} msg=${ack.payload.message_id} text_len=${text.length} media=${mediaFragments.length} trace=${ack.trace_id}`,
|
package/src/plugin-entry.test.ts
CHANGED
|
@@ -3,9 +3,14 @@ import pluginEntry from "../index.ts";
|
|
|
3
3
|
|
|
4
4
|
describe("openclaw-clawchat plugin entry", () => {
|
|
5
5
|
it("registers the channel/tools and native activation command without bootstrap migration", () => {
|
|
6
|
+
const mutateConfigFile = vi.fn();
|
|
6
7
|
const api = {
|
|
7
8
|
config: {},
|
|
8
|
-
runtime: {
|
|
9
|
+
runtime: {
|
|
10
|
+
config: {
|
|
11
|
+
mutateConfigFile,
|
|
12
|
+
},
|
|
13
|
+
},
|
|
9
14
|
logger: { debug: vi.fn(), info: vi.fn(), error: vi.fn() },
|
|
10
15
|
registerChannel: vi.fn(),
|
|
11
16
|
registerCommand: vi.fn(),
|
|
@@ -23,5 +28,6 @@ describe("openclaw-clawchat plugin entry", () => {
|
|
|
23
28
|
name: "clawchat-login",
|
|
24
29
|
}),
|
|
25
30
|
);
|
|
31
|
+
expect(mutateConfigFile).not.toHaveBeenCalled();
|
|
26
32
|
});
|
|
27
33
|
});
|
|
@@ -3,6 +3,82 @@ import { describe, expect, it, vi } from "vitest";
|
|
|
3
3
|
import { createOpenclawClawlingReplyDispatcher } from "./reply-dispatcher.ts";
|
|
4
4
|
|
|
5
5
|
describe("openclaw-clawchat reply-dispatcher", () => {
|
|
6
|
+
it("uses chat_id, not sender_id, as the consolidated streaming reply marker", async () => {
|
|
7
|
+
let hooks:
|
|
8
|
+
| {
|
|
9
|
+
deliver?: (payload: { text?: string }, info?: { kind: "tool" | "block" | "final" }) => Promise<void>;
|
|
10
|
+
onIdle?: () => Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
| undefined;
|
|
13
|
+
const sent: Array<{ event: string; payload: Record<string, unknown> }> = [];
|
|
14
|
+
const client = {
|
|
15
|
+
opts: {
|
|
16
|
+
transport: {
|
|
17
|
+
send: (data: string) => {
|
|
18
|
+
const env = JSON.parse(data) as { event: string; payload: Record<string, unknown> };
|
|
19
|
+
sent.push({ event: env.event, payload: env.payload });
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
traceIdFactory: () => "trace-chat-marker",
|
|
23
|
+
},
|
|
24
|
+
typing: vi.fn(),
|
|
25
|
+
} as unknown as ClawlingChatClient;
|
|
26
|
+
|
|
27
|
+
createOpenclawClawlingReplyDispatcher({
|
|
28
|
+
cfg: {} as never,
|
|
29
|
+
runtime: {
|
|
30
|
+
channel: {
|
|
31
|
+
reply: {
|
|
32
|
+
resolveHumanDelayConfig: vi.fn(() => ({ enabled: false })),
|
|
33
|
+
createReplyDispatcherWithTyping: vi.fn((options) => {
|
|
34
|
+
hooks = options;
|
|
35
|
+
return {
|
|
36
|
+
dispatcher: {},
|
|
37
|
+
replyOptions: {},
|
|
38
|
+
markDispatchIdle: vi.fn(),
|
|
39
|
+
};
|
|
40
|
+
}),
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
} as never,
|
|
44
|
+
account: {
|
|
45
|
+
accountId: "default",
|
|
46
|
+
userId: "agent-1",
|
|
47
|
+
replyMode: "stream",
|
|
48
|
+
forwardThinking: true,
|
|
49
|
+
forwardToolCalls: false,
|
|
50
|
+
stream: { flushIntervalMs: 250, minChunkChars: 1, maxBufferChars: 2000 },
|
|
51
|
+
} as never,
|
|
52
|
+
client,
|
|
53
|
+
target: { chatId: "chat-1", chatType: "direct" },
|
|
54
|
+
inboundMessageId: "inbound-1",
|
|
55
|
+
inboundForFinalReply: {
|
|
56
|
+
chatId: "chat-1",
|
|
57
|
+
senderId: "user-1",
|
|
58
|
+
senderNickName: "User 1",
|
|
59
|
+
bodyText: "hello",
|
|
60
|
+
},
|
|
61
|
+
log: { info: vi.fn(), error: vi.fn() },
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
await hooks?.deliver?.({ text: "agent reply" }, { kind: "final" });
|
|
65
|
+
await hooks?.onIdle?.();
|
|
66
|
+
|
|
67
|
+
const finalReply = sent.find((entry) => entry.event === "message.reply");
|
|
68
|
+
expect(finalReply?.payload).toMatchObject({
|
|
69
|
+
message: {
|
|
70
|
+
context: {
|
|
71
|
+
reply: {
|
|
72
|
+
reply_preview: {
|
|
73
|
+
id: "chat-1",
|
|
74
|
+
nick_name: "User 1",
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
6
82
|
it("emits message.failed in stream mode even if execution errors before any stream chunk", async () => {
|
|
7
83
|
let hooks:
|
|
8
84
|
| {
|
|
@@ -61,7 +137,7 @@ describe("openclaw-clawchat reply-dispatcher", () => {
|
|
|
61
137
|
expect(sent[0]!.event).toBe("message.failed");
|
|
62
138
|
expect(sent[0]!.payload.reason).toBe("Error: boom");
|
|
63
139
|
expect((client.typing as ReturnType<typeof vi.fn>).mock.calls).toEqual([
|
|
64
|
-
["chat-1", false
|
|
140
|
+
["chat-1", false],
|
|
65
141
|
]);
|
|
66
142
|
});
|
|
67
143
|
|
|
@@ -186,10 +262,10 @@ describe("openclaw-clawchat reply-dispatcher", () => {
|
|
|
186
262
|
expect(client.sendMessage).toHaveBeenCalledWith(
|
|
187
263
|
expect.objectContaining({
|
|
188
264
|
chat_id: "chat-1",
|
|
189
|
-
chat_type: "direct",
|
|
190
265
|
body: { fragments: [{ kind: "text", text: "Error: boom" }] },
|
|
191
266
|
}),
|
|
192
267
|
);
|
|
268
|
+
expect((client.sendMessage as ReturnType<typeof vi.fn>).mock.calls[0][0]).not.toHaveProperty("chat_type");
|
|
193
269
|
expect(client.replyMessage).not.toHaveBeenCalled();
|
|
194
270
|
});
|
|
195
271
|
|
|
@@ -246,9 +322,348 @@ describe("openclaw-clawchat reply-dispatcher", () => {
|
|
|
246
322
|
expect(client.sendMessage).toHaveBeenCalledWith(
|
|
247
323
|
expect.objectContaining({
|
|
248
324
|
chat_id: "chat-1",
|
|
249
|
-
chat_type: "direct",
|
|
250
325
|
body: { fragments: [{ kind: "text", text: "Error: boom" }] },
|
|
251
326
|
}),
|
|
252
327
|
);
|
|
328
|
+
expect((client.sendMessage as ReturnType<typeof vi.fn>).mock.calls[0][0]).not.toHaveProperty("chat_type");
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it("emits approval rich fragments with fallback_text when rich interactions are enabled", async () => {
|
|
332
|
+
let hooks:
|
|
333
|
+
| {
|
|
334
|
+
deliver?: (payload: {
|
|
335
|
+
text?: string;
|
|
336
|
+
presentation?: {
|
|
337
|
+
title?: string;
|
|
338
|
+
tone?: string;
|
|
339
|
+
blocks: Array<
|
|
340
|
+
| { type: "text"; text: string }
|
|
341
|
+
| { type: "buttons"; buttons: Array<{ label: string; value?: string; style?: string }> }
|
|
342
|
+
>;
|
|
343
|
+
};
|
|
344
|
+
}, info?: { kind: "tool" | "block" | "final" }) => Promise<void>;
|
|
345
|
+
}
|
|
346
|
+
| undefined;
|
|
347
|
+
const client = {
|
|
348
|
+
sendMessage: vi.fn().mockResolvedValue({
|
|
349
|
+
payload: { message_id: "server-m1", accepted_at: 1234 },
|
|
350
|
+
trace_id: "trace-rich",
|
|
351
|
+
}),
|
|
352
|
+
replyMessage: vi.fn(),
|
|
353
|
+
typing: vi.fn(),
|
|
354
|
+
} as unknown as ClawlingChatClient;
|
|
355
|
+
|
|
356
|
+
createOpenclawClawlingReplyDispatcher({
|
|
357
|
+
cfg: {} as never,
|
|
358
|
+
runtime: {
|
|
359
|
+
channel: {
|
|
360
|
+
reply: {
|
|
361
|
+
resolveHumanDelayConfig: vi.fn(() => ({ enabled: false })),
|
|
362
|
+
createReplyDispatcherWithTyping: vi.fn((options) => {
|
|
363
|
+
hooks = options;
|
|
364
|
+
return { dispatcher: {}, replyOptions: {}, markDispatchIdle: vi.fn() };
|
|
365
|
+
}),
|
|
366
|
+
},
|
|
367
|
+
},
|
|
368
|
+
} as never,
|
|
369
|
+
account: {
|
|
370
|
+
accountId: "default",
|
|
371
|
+
userId: "agent-1",
|
|
372
|
+
replyMode: "static",
|
|
373
|
+
forwardThinking: true,
|
|
374
|
+
forwardToolCalls: false,
|
|
375
|
+
richInteractions: true,
|
|
376
|
+
stream: { flushIntervalMs: 250, minChunkChars: 40, maxBufferChars: 2000 },
|
|
377
|
+
} as never,
|
|
378
|
+
client,
|
|
379
|
+
target: { chatId: "chat-1", chatType: "direct" },
|
|
380
|
+
log: { info: vi.fn(), error: vi.fn() },
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
await hooks?.deliver?.(
|
|
384
|
+
{
|
|
385
|
+
text: "Approve file deletion?",
|
|
386
|
+
presentation: {
|
|
387
|
+
title: "Approval required",
|
|
388
|
+
tone: "warning",
|
|
389
|
+
blocks: [
|
|
390
|
+
{ type: "text", text: "Delete /tmp/example.txt?" },
|
|
391
|
+
{
|
|
392
|
+
type: "buttons",
|
|
393
|
+
buttons: [
|
|
394
|
+
{ label: "Approve", value: "approve", style: "primary" },
|
|
395
|
+
{ label: "Deny", value: "deny", style: "danger" },
|
|
396
|
+
],
|
|
397
|
+
},
|
|
398
|
+
],
|
|
399
|
+
},
|
|
400
|
+
},
|
|
401
|
+
{ kind: "final" },
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
expect(client.sendMessage).toHaveBeenCalledWith(
|
|
405
|
+
expect.objectContaining({
|
|
406
|
+
body: {
|
|
407
|
+
fragments: [
|
|
408
|
+
{
|
|
409
|
+
kind: "approval_request",
|
|
410
|
+
title: "Approval required",
|
|
411
|
+
fallback_text: expect.stringContaining("Delete /tmp/example.txt?"),
|
|
412
|
+
state: "pending",
|
|
413
|
+
actions: [
|
|
414
|
+
{ id: "approve", label: "Approve", style: "primary", payload: { value: "approve" } },
|
|
415
|
+
{ id: "deny", label: "Deny", style: "danger", payload: { value: "deny" } },
|
|
416
|
+
],
|
|
417
|
+
},
|
|
418
|
+
],
|
|
419
|
+
},
|
|
420
|
+
}),
|
|
421
|
+
);
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
it("sends plain fallback text for presentations when rich interactions are disabled", async () => {
|
|
425
|
+
let hooks:
|
|
426
|
+
| {
|
|
427
|
+
deliver?: (payload: {
|
|
428
|
+
text?: string;
|
|
429
|
+
presentation?: {
|
|
430
|
+
title?: string;
|
|
431
|
+
blocks: Array<{ type: "text"; text: string }>;
|
|
432
|
+
};
|
|
433
|
+
}, info?: { kind: "tool" | "block" | "final" }) => Promise<void>;
|
|
434
|
+
}
|
|
435
|
+
| undefined;
|
|
436
|
+
const client = {
|
|
437
|
+
sendMessage: vi.fn().mockResolvedValue({
|
|
438
|
+
payload: { message_id: "server-m1", accepted_at: 1234 },
|
|
439
|
+
trace_id: "trace-fallback",
|
|
440
|
+
}),
|
|
441
|
+
replyMessage: vi.fn(),
|
|
442
|
+
typing: vi.fn(),
|
|
443
|
+
} as unknown as ClawlingChatClient;
|
|
444
|
+
|
|
445
|
+
createOpenclawClawlingReplyDispatcher({
|
|
446
|
+
cfg: {} as never,
|
|
447
|
+
runtime: {
|
|
448
|
+
channel: {
|
|
449
|
+
reply: {
|
|
450
|
+
resolveHumanDelayConfig: vi.fn(() => ({ enabled: false })),
|
|
451
|
+
createReplyDispatcherWithTyping: vi.fn((options) => {
|
|
452
|
+
hooks = options;
|
|
453
|
+
return { dispatcher: {}, replyOptions: {}, markDispatchIdle: vi.fn() };
|
|
454
|
+
}),
|
|
455
|
+
},
|
|
456
|
+
},
|
|
457
|
+
} as never,
|
|
458
|
+
account: {
|
|
459
|
+
accountId: "default",
|
|
460
|
+
userId: "agent-1",
|
|
461
|
+
replyMode: "static",
|
|
462
|
+
forwardThinking: true,
|
|
463
|
+
forwardToolCalls: false,
|
|
464
|
+
richInteractions: false,
|
|
465
|
+
stream: { flushIntervalMs: 250, minChunkChars: 40, maxBufferChars: 2000 },
|
|
466
|
+
} as never,
|
|
467
|
+
client,
|
|
468
|
+
target: { chatId: "chat-1", chatType: "direct" },
|
|
469
|
+
log: { info: vi.fn(), error: vi.fn() },
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
await hooks?.deliver?.(
|
|
473
|
+
{
|
|
474
|
+
text: "Approve file deletion?",
|
|
475
|
+
presentation: {
|
|
476
|
+
title: "Approval required",
|
|
477
|
+
blocks: [{ type: "text", text: "Delete /tmp/example.txt?" }],
|
|
478
|
+
},
|
|
479
|
+
},
|
|
480
|
+
{ kind: "final" },
|
|
481
|
+
);
|
|
482
|
+
|
|
483
|
+
expect(client.sendMessage).toHaveBeenCalledWith(
|
|
484
|
+
expect.objectContaining({
|
|
485
|
+
body: {
|
|
486
|
+
fragments: [{ kind: "text", text: expect.stringContaining("Delete /tmp/example.txt?") }],
|
|
487
|
+
},
|
|
488
|
+
}),
|
|
489
|
+
);
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
it("includes rich interaction fragments in the consolidated streaming final reply", async () => {
|
|
493
|
+
let hooks:
|
|
494
|
+
| {
|
|
495
|
+
deliver?: (payload: {
|
|
496
|
+
text?: string;
|
|
497
|
+
presentation?: {
|
|
498
|
+
title?: string;
|
|
499
|
+
tone?: string;
|
|
500
|
+
blocks: Array<
|
|
501
|
+
| { type: "text"; text: string }
|
|
502
|
+
| { type: "buttons"; buttons: Array<{ label: string; value?: string }> }
|
|
503
|
+
>;
|
|
504
|
+
};
|
|
505
|
+
}, info?: { kind: "tool" | "block" | "final" }) => Promise<void>;
|
|
506
|
+
onIdle?: () => Promise<void>;
|
|
507
|
+
}
|
|
508
|
+
| undefined;
|
|
509
|
+
const sent: Array<{ event: string; payload: Record<string, unknown> }> = [];
|
|
510
|
+
const client = {
|
|
511
|
+
opts: {
|
|
512
|
+
transport: {
|
|
513
|
+
send: (data: string) => {
|
|
514
|
+
const env = JSON.parse(data) as { event: string; payload: Record<string, unknown> };
|
|
515
|
+
sent.push({ event: env.event, payload: env.payload });
|
|
516
|
+
},
|
|
517
|
+
},
|
|
518
|
+
traceIdFactory: () => "trace-stream-rich",
|
|
519
|
+
},
|
|
520
|
+
typing: vi.fn(),
|
|
521
|
+
} as unknown as ClawlingChatClient;
|
|
522
|
+
|
|
523
|
+
createOpenclawClawlingReplyDispatcher({
|
|
524
|
+
cfg: {} as never,
|
|
525
|
+
runtime: {
|
|
526
|
+
channel: {
|
|
527
|
+
reply: {
|
|
528
|
+
resolveHumanDelayConfig: vi.fn(() => ({ enabled: false })),
|
|
529
|
+
createReplyDispatcherWithTyping: vi.fn((options) => {
|
|
530
|
+
hooks = options;
|
|
531
|
+
return { dispatcher: {}, replyOptions: {}, markDispatchIdle: vi.fn() };
|
|
532
|
+
}),
|
|
533
|
+
},
|
|
534
|
+
},
|
|
535
|
+
} as never,
|
|
536
|
+
account: {
|
|
537
|
+
accountId: "default",
|
|
538
|
+
userId: "agent-1",
|
|
539
|
+
replyMode: "stream",
|
|
540
|
+
forwardThinking: true,
|
|
541
|
+
forwardToolCalls: false,
|
|
542
|
+
richInteractions: true,
|
|
543
|
+
stream: { flushIntervalMs: 250, minChunkChars: 1, maxBufferChars: 2000 },
|
|
544
|
+
} as never,
|
|
545
|
+
client,
|
|
546
|
+
target: { chatId: "chat-1", chatType: "direct" },
|
|
547
|
+
inboundMessageId: "inbound-1",
|
|
548
|
+
inboundForFinalReply: {
|
|
549
|
+
chatId: "chat-1",
|
|
550
|
+
senderId: "user-1",
|
|
551
|
+
senderNickName: "User 1",
|
|
552
|
+
bodyText: "hello",
|
|
553
|
+
},
|
|
554
|
+
log: { info: vi.fn(), error: vi.fn() },
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
await hooks?.deliver?.(
|
|
558
|
+
{
|
|
559
|
+
presentation: {
|
|
560
|
+
title: "Approval required",
|
|
561
|
+
tone: "warning",
|
|
562
|
+
blocks: [
|
|
563
|
+
{ type: "text", text: "Run deploy?" },
|
|
564
|
+
{ type: "buttons", buttons: [{ label: "Approve", value: "approve" }] },
|
|
565
|
+
],
|
|
566
|
+
},
|
|
567
|
+
},
|
|
568
|
+
{ kind: "final" },
|
|
569
|
+
);
|
|
570
|
+
await hooks?.onIdle?.();
|
|
571
|
+
|
|
572
|
+
const finalReply = sent.find((entry) => entry.event === "message.reply");
|
|
573
|
+
expect(finalReply?.payload).toMatchObject({
|
|
574
|
+
message: {
|
|
575
|
+
body: {
|
|
576
|
+
fragments: [
|
|
577
|
+
{
|
|
578
|
+
kind: "approval_request",
|
|
579
|
+
title: "Approval required",
|
|
580
|
+
fallback_text: expect.stringContaining("Run deploy?"),
|
|
581
|
+
state: "pending",
|
|
582
|
+
},
|
|
583
|
+
],
|
|
584
|
+
},
|
|
585
|
+
},
|
|
586
|
+
});
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
it("forwards non-final rich block payloads in static mode", async () => {
|
|
590
|
+
let hooks:
|
|
591
|
+
| {
|
|
592
|
+
deliver?: (payload: {
|
|
593
|
+
text?: string;
|
|
594
|
+
presentation?: {
|
|
595
|
+
title?: string;
|
|
596
|
+
blocks: Array<
|
|
597
|
+
| { type: "text"; text: string }
|
|
598
|
+
| { type: "buttons"; buttons: Array<{ label: string; value?: string }> }
|
|
599
|
+
>;
|
|
600
|
+
};
|
|
601
|
+
}, info?: { kind: "tool" | "block" | "final" }) => Promise<void>;
|
|
602
|
+
}
|
|
603
|
+
| undefined;
|
|
604
|
+
const client = {
|
|
605
|
+
sendMessage: vi.fn().mockResolvedValue({
|
|
606
|
+
payload: { message_id: "server-m1", accepted_at: 1234 },
|
|
607
|
+
trace_id: "trace-block-rich",
|
|
608
|
+
}),
|
|
609
|
+
replyMessage: vi.fn(),
|
|
610
|
+
typing: vi.fn(),
|
|
611
|
+
} as unknown as ClawlingChatClient;
|
|
612
|
+
|
|
613
|
+
createOpenclawClawlingReplyDispatcher({
|
|
614
|
+
cfg: {} as never,
|
|
615
|
+
runtime: {
|
|
616
|
+
channel: {
|
|
617
|
+
reply: {
|
|
618
|
+
resolveHumanDelayConfig: vi.fn(() => ({ enabled: false })),
|
|
619
|
+
createReplyDispatcherWithTyping: vi.fn((options) => {
|
|
620
|
+
hooks = options;
|
|
621
|
+
return { dispatcher: {}, replyOptions: {}, markDispatchIdle: vi.fn() };
|
|
622
|
+
}),
|
|
623
|
+
},
|
|
624
|
+
},
|
|
625
|
+
} as never,
|
|
626
|
+
account: {
|
|
627
|
+
accountId: "default",
|
|
628
|
+
userId: "agent-1",
|
|
629
|
+
replyMode: "static",
|
|
630
|
+
forwardThinking: true,
|
|
631
|
+
forwardToolCalls: false,
|
|
632
|
+
richInteractions: true,
|
|
633
|
+
stream: { flushIntervalMs: 250, minChunkChars: 40, maxBufferChars: 2000 },
|
|
634
|
+
} as never,
|
|
635
|
+
client,
|
|
636
|
+
target: { chatId: "chat-1", chatType: "direct" },
|
|
637
|
+
log: { info: vi.fn(), error: vi.fn() },
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
await hooks?.deliver?.(
|
|
641
|
+
{
|
|
642
|
+
presentation: {
|
|
643
|
+
title: "Choose next step",
|
|
644
|
+
blocks: [
|
|
645
|
+
{ type: "text", text: "Pick an action" },
|
|
646
|
+
{ type: "buttons", buttons: [{ label: "Continue", value: "continue" }] },
|
|
647
|
+
],
|
|
648
|
+
},
|
|
649
|
+
},
|
|
650
|
+
{ kind: "block" },
|
|
651
|
+
);
|
|
652
|
+
|
|
653
|
+
expect(client.sendMessage).toHaveBeenCalledWith(
|
|
654
|
+
expect.objectContaining({
|
|
655
|
+
body: {
|
|
656
|
+
fragments: [
|
|
657
|
+
{
|
|
658
|
+
kind: "action_card",
|
|
659
|
+
title: "Choose next step",
|
|
660
|
+
fallback_text: expect.stringContaining("Pick an action"),
|
|
661
|
+
state: "pending",
|
|
662
|
+
actions: [{ id: "continue", label: "Continue", payload: { value: "continue" } }],
|
|
663
|
+
},
|
|
664
|
+
],
|
|
665
|
+
},
|
|
666
|
+
}),
|
|
667
|
+
);
|
|
253
668
|
});
|
|
254
669
|
});
|