mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-03 14:54:05 +00:00
fix(googlechat): preserve thread for message tool replies (#80996)
Use the Google Chat thread resource as the ambient message-tool reply target so replies stay in the inbound thread. Normalize the current Google Chat space target and let plugin threading adapters explicitly suppress the generic message-id fallback when a provider needs a thread resource instead of a message resource. Co-authored-by: Peter Steinberger <steipete@gmail.com> Co-authored-by: Franco Viotti <franco-viotti@users.noreply.github.com>
This commit is contained in:
@@ -1,4 +1,8 @@
|
||||
import { adaptScopedAccountAccessor } from "openclaw/plugin-sdk/channel-config-helpers";
|
||||
import type {
|
||||
ChannelThreadingContext,
|
||||
ChannelThreadingToolContext,
|
||||
} from "openclaw/plugin-sdk/channel-contract";
|
||||
import {
|
||||
createMessageReceiptFromOutboundResults,
|
||||
defineChannelMessageAdapter,
|
||||
@@ -127,6 +131,29 @@ export const googlechatThreadingAdapter = {
|
||||
account.config.replyToMode,
|
||||
fallback: "off" as const,
|
||||
},
|
||||
buildToolContext: ({
|
||||
cfg,
|
||||
accountId,
|
||||
context,
|
||||
hasRepliedRef,
|
||||
}: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
context: ChannelThreadingContext;
|
||||
hasRepliedRef?: { value: boolean };
|
||||
}): ChannelThreadingToolContext => {
|
||||
const currentChannelId = normalizeGoogleChatTarget(context.To);
|
||||
const replyToId =
|
||||
normalizeOptionalString(context.ReplyToIdFull) ?? normalizeOptionalString(context.ReplyToId);
|
||||
|
||||
return {
|
||||
currentChannelId,
|
||||
currentMessageId: replyToId,
|
||||
currentThreadTs: replyToId,
|
||||
replyToMode: resolveGoogleChatAccount({ cfg, accountId }).config.replyToMode,
|
||||
hasRepliedRef,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const googlechatPairingTextAdapter = {
|
||||
|
||||
@@ -439,6 +439,62 @@ describe("googlechatPlugin threading", () => {
|
||||
googlechatThreadingAdapter.scopedAccountReplyToMode.resolveReplyToMode(defaultAccount),
|
||||
).toBe("all");
|
||||
});
|
||||
|
||||
it("uses the inbound thread resource as the current tool reply target", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
googlechat: {
|
||||
replyToMode: "all",
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
const hasRepliedRef = { value: false };
|
||||
|
||||
const context = googlechatThreadingAdapter.buildToolContext({
|
||||
cfg,
|
||||
accountId: "default",
|
||||
context: {
|
||||
To: "googlechat:spaces/AAA",
|
||||
CurrentMessageId: "spaces/AAA/messages/msg-1",
|
||||
ReplyToId: "spaces/AAA/threads/thread-1",
|
||||
},
|
||||
hasRepliedRef,
|
||||
});
|
||||
|
||||
expect(context).toMatchObject({
|
||||
currentChannelId: "spaces/AAA",
|
||||
currentMessageId: "spaces/AAA/threads/thread-1",
|
||||
currentThreadTs: "spaces/AAA/threads/thread-1",
|
||||
replyToMode: "all",
|
||||
hasRepliedRef,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not use message resources as implicit Google Chat reply targets", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
googlechat: {
|
||||
replyToMode: "all",
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const context = googlechatThreadingAdapter.buildToolContext({
|
||||
cfg,
|
||||
accountId: "default",
|
||||
context: {
|
||||
To: "googlechat:spaces/AAA",
|
||||
CurrentMessageId: "spaces/AAA/messages/msg-1",
|
||||
},
|
||||
});
|
||||
|
||||
expect(context).toMatchObject({
|
||||
currentChannelId: "spaces/AAA",
|
||||
replyToMode: "all",
|
||||
});
|
||||
expect(context.currentMessageId).toBeUndefined();
|
||||
expect(context.currentThreadTs).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
const resolveTarget = googlechatOutboundAdapter.base.resolveTarget;
|
||||
|
||||
@@ -152,6 +152,7 @@ export function buildThreadingToolContext(params: {
|
||||
ChatType: sessionCtx.ChatType,
|
||||
CurrentMessageId: currentMessageId,
|
||||
ReplyToId: sessionCtx.ReplyToId,
|
||||
ReplyToIdFull: sessionCtx.ReplyToIdFull,
|
||||
ThreadLabel: sessionCtx.ThreadLabel,
|
||||
MessageThreadId: sessionCtx.MessageThreadId,
|
||||
TransportThreadId: sessionCtx.TransportThreadId,
|
||||
@@ -159,10 +160,13 @@ export function buildThreadingToolContext(params: {
|
||||
},
|
||||
hasRepliedRef,
|
||||
}) ?? {};
|
||||
const hasAdapterCurrentMessageId = Object.hasOwn(context, "currentMessageId");
|
||||
return {
|
||||
...context,
|
||||
currentChannelProvider: provider!, // guaranteed non-null since threading exists
|
||||
currentMessageId: context.currentMessageId ?? currentMessageId,
|
||||
// Some providers expose only thread resources as reply targets; explicit
|
||||
// `undefined` means the adapter rejected the generic message-id fallback.
|
||||
currentMessageId: hasAdapterCurrentMessageId ? context.currentMessageId : currentMessageId,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -193,6 +193,44 @@ describe("buildThreadingToolContext", () => {
|
||||
expect(result.currentChannelId).toBe("C1");
|
||||
expect(result.currentThreadTs).toBe("123.456");
|
||||
});
|
||||
|
||||
it("lets plugin threading adapters suppress the generic message-id fallback", () => {
|
||||
setActivePluginRegistry(
|
||||
createTestRegistry([
|
||||
{
|
||||
pluginId: "googlechat",
|
||||
plugin: {
|
||||
...createChannelTestPluginBase({ id: "googlechat", label: "Google Chat" }),
|
||||
threading: {
|
||||
buildToolContext: ({ context }) => ({
|
||||
currentChannelId: context.To?.replace(/^googlechat:/, ""),
|
||||
currentMessageId: undefined,
|
||||
currentThreadTs: context.ReplyToIdFull ?? context.ReplyToId,
|
||||
}),
|
||||
},
|
||||
} as ChannelPlugin,
|
||||
source: "test",
|
||||
},
|
||||
]),
|
||||
);
|
||||
const sessionCtx = {
|
||||
Provider: "googlechat",
|
||||
To: "googlechat:spaces/AAA",
|
||||
MessageSidFull: "spaces/AAA/messages/msg-1",
|
||||
ReplyToId: "spaces/AAA/threads/short",
|
||||
ReplyToIdFull: "spaces/AAA/threads/full",
|
||||
} as TemplateContext;
|
||||
|
||||
const result = buildThreadingToolContext({
|
||||
sessionCtx,
|
||||
config: { channels: { googlechat: { replyToMode: "all" } } } as OpenClawConfig,
|
||||
hasRepliedRef: undefined,
|
||||
});
|
||||
|
||||
expect(result.currentChannelId).toBe("spaces/AAA");
|
||||
expect(result.currentThreadTs).toBe("spaces/AAA/threads/full");
|
||||
expect(result.currentMessageId).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("applyReplyThreading auto-threading", () => {
|
||||
|
||||
Reference in New Issue
Block a user