

























@@ -86,6 +86,16 @@ vi.mock("../send.js", () => ({
8686},
8787}));
888889+const typingMocks = vi.hoisted(() => ({
90+sendTyping: vi.fn<(params: { rest: unknown; channelId: string }) => Promise<void>>(
91+async () => {},
92+),
93+}));
94+95+vi.mock("./typing.js", () => ({
96+sendTyping: typingMocks.sendTyping,
97+}));
98+8999const discordTargetMocks = vi.hoisted(() => ({
90100resolveDiscordTargetChannelId: vi.fn(async (target: string, _opts?: unknown) => ({
91101channelId: target === "user:u1" ? "dm-u1" : target,
@@ -169,6 +179,7 @@ type DispatchInboundParams = {
169179onPartialReply?: (payload: { text?: string }) => Promise<void> | void;
170180onAssistantMessageStart?: () => Promise<void> | void;
171181allowProgressCallbacksWhenSourceDeliverySuppressed?: boolean;
182+onTypingCleanup?: () => Promise<void> | void;
172183};
173184};
174185const dispatchInboundMessage = vi.hoisted(() =>
@@ -233,6 +244,7 @@ let createThreadBindingManager: typeof import("./thread-bindings.js").createThre
233244let processDiscordMessage: typeof import("./message-handler.process.js").processDiscordMessage;
234245let formatDiscordReplySkip: typeof import("./message-handler.process.js").formatDiscordReplySkip;
235246let notifyDiscordInboundEventOutboundSuccess: typeof import("../inbound-event-delivery.js").notifyDiscordInboundEventOutboundSuccess;
247+let createDiscordReplyTypingFeedback: typeof import("./reply-typing-feedback.js").createDiscordReplyTypingFeedback;
236248237249vi.mock("openclaw/plugin-sdk/reply-runtime", () => ({
238250dispatchReplyWithBufferedBlockDispatcher: async (params: {
@@ -244,6 +256,14 @@ vi.mock("openclaw/plugin-sdk/reply-runtime", () => ({
244256deliver: (payload: unknown, info: { kind: "block" | "final" }) => Promise<void> | void;
245257onError?: (err: unknown, info: { kind: "block" | "final" }) => void;
246258transformReplyPayload?: (payload: ReplyPayload) => ReplyPayload | null;
259+typingCallbacks?: {
260+onReplyStart?: () => Promise<void> | void;
261+onIdle?: () => void;
262+onCleanup?: () => void;
263+};
264+onReplyStart?: () => Promise<void> | void;
265+onIdle?: () => void;
266+onCleanup?: () => void;
247267onSettled?: () => unknown;
248268onFreshSettledDelivery?: () => unknown;
249269};
@@ -273,10 +293,16 @@ vi.mock("openclaw/plugin-sdk/reply-runtime", () => ({
273293pendingDeliveries.push(delivery);
274294return true;
275295};
296+const typingCallbacks = params.dispatcherOptions.typingCallbacks;
297+const replyOptions = {
298+ ...params.replyOptions,
299+onReplyStart: params.dispatcherOptions.onReplyStart ?? typingCallbacks?.onReplyStart,
300+onTypingCleanup: params.dispatcherOptions.onCleanup ?? typingCallbacks?.onCleanup,
301+};
276302try {
277303return await dispatchInboundMessage({
278304ctx: params.ctx,
279-replyOptions: params.replyOptions,
305+ replyOptions,
280306dispatcher: {
281307sendBlockReply: vi.fn((payload: ReplyPayload) =>
282308queueDelivery(payload, { kind: "block" }),
@@ -292,6 +318,8 @@ vi.mock("openclaw/plugin-sdk/reply-runtime", () => ({
292318} finally {
293319await params.dispatcherOptions.onSettled?.();
294320await params.dispatcherOptions.onFreshSettledDelivery?.();
321+params.dispatcherOptions.onIdle?.();
322+typingCallbacks?.onIdle?.();
295323}
296324},
297325dispatchInboundMessage: (params: DispatchInboundParams) => dispatchInboundMessage(params),
@@ -456,12 +484,15 @@ beforeAll(async () => {
456484({ processDiscordMessage, formatDiscordReplySkip } =
457485await import("./message-handler.process.js"));
458486({ notifyDiscordInboundEventOutboundSuccess } = await import("../inbound-event-delivery.js"));
487+({ createDiscordReplyTypingFeedback } = await import("./reply-typing-feedback.js"));
459488});
460489461490beforeEach(() => {
462491vi.useRealTimers();
463492sendMocks.reactMessageDiscord.mockClear();
464493sendMocks.removeReactionDiscord.mockClear();
494+typingMocks.sendTyping.mockClear();
495+typingMocks.sendTyping.mockResolvedValue(undefined);
465496discordTargetMocks.resolveDiscordTargetChannelId.mockClear();
466497editMessageDiscord.mockClear();
467498deliverDiscordReply.mockClear();
@@ -873,6 +904,70 @@ describe("processDiscordMessage ack reactions", () => {
873904expect(feedbackRest).not.toBe(deliveryRest);
874905});
875906907+it("reuses accepted typing feedback through reply dispatch", async () => {
908+const replyTypingFeedback = {
909+onReplyStart: vi.fn(async () => {}),
910+onIdle: vi.fn(),
911+onCleanup: vi.fn(),
912+updateChannelId: vi.fn(),
913+getChannelId: vi.fn(() => "c1"),
914+restartForDispatch: vi.fn(),
915+};
916+dispatchInboundMessage.mockImplementationOnce(async (params?: DispatchInboundParams) => {
917+await params?.replyOptions?.onReplyStart?.();
918+return createNoQueuedDispatchResult();
919+});
920+const ctx = await createAutomaticSourceDeliveryContext({
921+ replyTypingFeedback,
922+});
923+924+await runProcessDiscordMessage(ctx);
925+926+expect(replyTypingFeedback.updateChannelId).not.toHaveBeenCalled();
927+expect(replyTypingFeedback.restartForDispatch).toHaveBeenCalledWith("c1");
928+expect(replyTypingFeedback.onReplyStart).toHaveBeenCalledTimes(1);
929+expect(replyTypingFeedback.onIdle).toHaveBeenCalledTimes(1);
930+expect(replyTypingFeedback.onCleanup).toHaveBeenCalledTimes(1);
931+expect(typingMocks.sendTyping).not.toHaveBeenCalled();
932+});
933+934+it("restarts stale carried typing feedback before dispatch", async () => {
935+vi.useFakeTimers();
936+const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
937+const rest = { kind: "feedback-rest" };
938+try {
939+dispatchInboundMessage.mockImplementationOnce(async (params?: DispatchInboundParams) => {
940+await params?.replyOptions?.onReplyStart?.();
941+await vi.advanceTimersByTimeAsync(3_500);
942+return createNoQueuedDispatchResult();
943+});
944+const ctx = await createAutomaticSourceDeliveryContext();
945+ctx.replyTypingFeedback = createDiscordReplyTypingFeedback({
946+cfg: ctx.cfg,
947+token: ctx.token,
948+accountId: ctx.accountId,
949+channelId: "c1",
950+rest: rest as never,
951+log: vi.fn(),
952+maxDurationMs: 5_000,
953+});
954+await ctx.replyTypingFeedback.onReplyStart();
955+await vi.advanceTimersByTimeAsync(5_100);
956+typingMocks.sendTyping.mockClear();
957+958+await runProcessDiscordMessage(ctx);
959+960+expect(typingMocks.sendTyping.mock.calls.length).toBeGreaterThanOrEqual(2);
961+expect(
962+typingMocks.sendTyping.mock.calls.every(
963+([params]) => params.channelId === "c1" && params.rest === rest,
964+),
965+).toBe(true);
966+} finally {
967+warnSpy.mockRestore();
968+}
969+});
970+876971it("debounces intermediate phase reactions and jumps to done for short runs", async () => {
877972dispatchInboundMessage.mockImplementationOnce(async (params?: DispatchInboundParams) => {
878973await params?.replyOptions?.onReasoningStream?.();
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。