mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 16:20:43 +00:00
refactor: migrate bundled plugins to message lifecycle
This commit is contained in:
@@ -21,7 +21,7 @@ export {
|
||||
runPassiveAccountLifecycle,
|
||||
} from "openclaw/plugin-sdk/channel-lifecycle";
|
||||
export { createChannelPairingController } from "openclaw/plugin-sdk/channel-pairing";
|
||||
export { createChannelReplyPipeline } from "openclaw/plugin-sdk/channel-reply-pipeline";
|
||||
export { createChannelMessageReplyPipeline } from "openclaw/plugin-sdk/channel-message";
|
||||
export {
|
||||
evaluateGroupRouteAccessForPolicy,
|
||||
resolveDmGroupAccessWithLists,
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { adaptScopedAccountAccessor } from "openclaw/plugin-sdk/channel-config-helpers";
|
||||
import {
|
||||
createMessageReceiptFromOutboundResults,
|
||||
defineChannelMessageAdapter,
|
||||
type MessageReceiptPartKind,
|
||||
} from "openclaw/plugin-sdk/channel-message";
|
||||
import {
|
||||
composeAccountWarningCollectors,
|
||||
createAllowlistProviderOpenWarningCollector,
|
||||
@@ -36,6 +41,28 @@ const loadGoogleChatChannelRuntime = createLazyRuntimeNamedExport(
|
||||
"googleChatChannelRuntime",
|
||||
);
|
||||
|
||||
function createGoogleChatSendReceipt(params: {
|
||||
messageId?: string;
|
||||
chatId: string;
|
||||
kind: MessageReceiptPartKind;
|
||||
}) {
|
||||
const messageId = params.messageId?.trim();
|
||||
return createMessageReceiptFromOutboundResults({
|
||||
results: messageId
|
||||
? [
|
||||
{
|
||||
channel: "googlechat",
|
||||
messageId,
|
||||
chatId: params.chatId,
|
||||
conversationId: params.chatId,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
threadId: params.chatId,
|
||||
kind: params.kind,
|
||||
});
|
||||
}
|
||||
|
||||
export const formatAllowFromEntry = (entry: string) =>
|
||||
normalizeLowercaseStringOrEmpty(
|
||||
entry
|
||||
@@ -200,9 +227,11 @@ export const googlechatOutboundAdapter = {
|
||||
text,
|
||||
thread,
|
||||
});
|
||||
const messageId = result?.messageName ?? "";
|
||||
return {
|
||||
messageId: result?.messageName ?? "",
|
||||
messageId,
|
||||
chatId: space,
|
||||
receipt: createGoogleChatSendReceipt({ messageId, chatId: space, kind: "text" }),
|
||||
};
|
||||
},
|
||||
sendMedia: async ({
|
||||
@@ -284,10 +313,28 @@ export const googlechatOutboundAdapter = {
|
||||
]
|
||||
: undefined,
|
||||
});
|
||||
const messageId = result?.messageName ?? "";
|
||||
return {
|
||||
messageId: result?.messageName ?? "",
|
||||
messageId,
|
||||
chatId: space,
|
||||
receipt: createGoogleChatSendReceipt({ messageId, chatId: space, kind: "media" }),
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const googlechatMessageAdapter = defineChannelMessageAdapter({
|
||||
id: "googlechat",
|
||||
durableFinal: {
|
||||
capabilities: {
|
||||
text: true,
|
||||
media: true,
|
||||
thread: true,
|
||||
messageSendingHooks: true,
|
||||
},
|
||||
},
|
||||
send: {
|
||||
text: googlechatOutboundAdapter.attachedResults.sendText,
|
||||
media: googlechatOutboundAdapter.attachedResults.sendMedia,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { verifyChannelMessageAdapterCapabilityProofs } from "openclaw/plugin-sdk/channel-message";
|
||||
import {
|
||||
createDirectoryTestRuntime,
|
||||
expectDirectorySurface,
|
||||
@@ -6,6 +7,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../runtime-api.js";
|
||||
import {
|
||||
googlechatDirectoryAdapter,
|
||||
googlechatMessageAdapter,
|
||||
googlechatOutboundAdapter,
|
||||
googlechatPairingTextAdapter,
|
||||
googlechatSecurityAdapter,
|
||||
@@ -206,6 +208,70 @@ function setupRuntimeMediaMocks(params: { loadFileName: string; loadBytes: strin
|
||||
}
|
||||
|
||||
describe("googlechatPlugin outbound sendMedia", () => {
|
||||
it("declares message adapter durable text, media, and thread with receipt proofs", async () => {
|
||||
sendGoogleChatMessageMock.mockResolvedValue({
|
||||
messageName: "spaces/AAA/messages/msg-1",
|
||||
});
|
||||
uploadGoogleChatAttachmentMock.mockResolvedValue({
|
||||
attachmentUploadToken: "token-1",
|
||||
});
|
||||
|
||||
const cfg = createGoogleChatCfg();
|
||||
|
||||
await expect(
|
||||
verifyChannelMessageAdapterCapabilityProofs({
|
||||
adapterName: "googlechat",
|
||||
adapter: googlechatMessageAdapter,
|
||||
proofs: {
|
||||
text: async () => {
|
||||
const result = await googlechatMessageAdapter.send?.text?.({
|
||||
cfg,
|
||||
to: "spaces/AAA",
|
||||
text: "hello",
|
||||
});
|
||||
expect(result?.receipt.parts[0]?.kind).toBe("text");
|
||||
expect(result?.receipt.platformMessageIds).toEqual(["spaces/AAA/messages/msg-1"]);
|
||||
},
|
||||
media: async () => {
|
||||
const result = await googlechatMessageAdapter.send?.media?.({
|
||||
cfg,
|
||||
to: "spaces/AAA",
|
||||
text: "image",
|
||||
mediaUrl: "https://example.com/img.png",
|
||||
});
|
||||
expect(result?.receipt.parts[0]?.kind).toBe("media");
|
||||
expect(result?.receipt.platformMessageIds).toEqual(["spaces/AAA/messages/msg-1"]);
|
||||
},
|
||||
thread: async () => {
|
||||
sendGoogleChatMessageMock.mockClear();
|
||||
await googlechatMessageAdapter.send?.text?.({
|
||||
cfg,
|
||||
to: "spaces/AAA",
|
||||
text: "threaded",
|
||||
threadId: "thread-1",
|
||||
});
|
||||
expect(sendGoogleChatMessageMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
space: "spaces/AAA",
|
||||
thread: "thread-1",
|
||||
}),
|
||||
);
|
||||
},
|
||||
messageSendingHooks: () => {
|
||||
expect(googlechatMessageAdapter.send?.text).toBeTypeOf("function");
|
||||
},
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.arrayContaining([
|
||||
{ capability: "text", status: "verified" },
|
||||
{ capability: "media", status: "verified" },
|
||||
{ capability: "thread", status: "verified" },
|
||||
{ capability: "messageSendingHooks", status: "verified" },
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("chunks outbound text without requiring Google Chat runtime initialization", () => {
|
||||
const chunker = googlechatOutboundAdapter.base.chunker;
|
||||
|
||||
@@ -256,10 +322,11 @@ describe("googlechatPlugin outbound sendMedia", () => {
|
||||
text: "caption",
|
||||
}),
|
||||
);
|
||||
expect(result).toEqual({
|
||||
expect(result).toMatchObject({
|
||||
messageId: "spaces/AAA/messages/msg-1",
|
||||
chatId: "spaces/AAA",
|
||||
});
|
||||
expect(result.receipt.primaryPlatformMessageId).toBe("spaces/AAA/messages/msg-1");
|
||||
});
|
||||
|
||||
it("keeps remote URL media fetch on fetchRemoteMedia with maxBytes cap", async () => {
|
||||
@@ -305,10 +372,11 @@ describe("googlechatPlugin outbound sendMedia", () => {
|
||||
text: "caption",
|
||||
}),
|
||||
);
|
||||
expect(result).toEqual({
|
||||
expect(result).toMatchObject({
|
||||
messageId: "spaces/AAA/messages/msg-2",
|
||||
chatId: "spaces/AAA",
|
||||
});
|
||||
expect(result.receipt.primaryPlatformMessageId).toBe("spaces/AAA/messages/msg-2");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -572,7 +640,7 @@ describe("googlechatPlugin outbound cfg threading", () => {
|
||||
mediaLocalRoots: ["/tmp/workspace"],
|
||||
accountId: "default",
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
).resolves.toMatchObject({
|
||||
messageId: "spaces/AAA/messages/msg-cold",
|
||||
chatId: "spaces/AAA",
|
||||
});
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
formatAllowFromEntry,
|
||||
googlechatDirectoryAdapter,
|
||||
googlechatGroupsAdapter,
|
||||
googlechatMessageAdapter,
|
||||
googlechatOutboundAdapter,
|
||||
googlechatPairingTextAdapter,
|
||||
googlechatSecurityAdapter,
|
||||
@@ -155,6 +156,7 @@ export const googlechatPlugin = createChatChannelPlugin({
|
||||
},
|
||||
},
|
||||
directory: googlechatDirectoryAdapter,
|
||||
message: googlechatMessageAdapter,
|
||||
resolver: {
|
||||
resolveTargets: async ({ inputs, kind }) => {
|
||||
const resolved = inputs.map((input) => {
|
||||
|
||||
39
extensions/googlechat/src/monitor-durable.test.ts
Normal file
39
extensions/googlechat/src/monitor-durable.test.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveGoogleChatDurableReplyOptions } from "./monitor-durable.js";
|
||||
|
||||
describe("resolveGoogleChatDurableReplyOptions", () => {
|
||||
it("enables durable final delivery when no typing preview is active", () => {
|
||||
expect(
|
||||
resolveGoogleChatDurableReplyOptions({
|
||||
payload: { text: "hello", replyToId: "thread-1" },
|
||||
infoKind: "final",
|
||||
spaceId: "spaces/AAA",
|
||||
}),
|
||||
).toEqual({
|
||||
to: "spaces/AAA",
|
||||
replyToId: "thread-1",
|
||||
threadId: "thread-1",
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps typing preview delivery on the legacy edit path", () => {
|
||||
expect(
|
||||
resolveGoogleChatDurableReplyOptions({
|
||||
payload: { text: "hello" },
|
||||
infoKind: "final",
|
||||
spaceId: "spaces/AAA",
|
||||
typingMessageName: "spaces/AAA/messages/typing",
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("does not durable-deliver non-final chunks", () => {
|
||||
expect(
|
||||
resolveGoogleChatDurableReplyOptions({
|
||||
payload: { text: "hello" },
|
||||
infoKind: "block",
|
||||
spaceId: "spaces/AAA",
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
23
extensions/googlechat/src/monitor-durable.ts
Normal file
23
extensions/googlechat/src/monitor-durable.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import type { ReplyPayload } from "openclaw/plugin-sdk/reply-runtime";
|
||||
|
||||
export type GoogleChatDurableReplyOptions = {
|
||||
to: string;
|
||||
replyToId?: string;
|
||||
threadId?: string;
|
||||
};
|
||||
|
||||
export function resolveGoogleChatDurableReplyOptions(params: {
|
||||
payload: ReplyPayload;
|
||||
infoKind: string;
|
||||
spaceId: string;
|
||||
typingMessageName?: string;
|
||||
}): GoogleChatDurableReplyOptions | false {
|
||||
if (params.infoKind !== "final" || params.typingMessageName) {
|
||||
return false;
|
||||
}
|
||||
const threadId = params.payload.replyToId?.trim() || undefined;
|
||||
return {
|
||||
to: params.spaceId,
|
||||
...(threadId ? { replyToId: threadId, threadId } : {}),
|
||||
};
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/text-runtime";
|
||||
import type { OpenClawConfig } from "../runtime-api.js";
|
||||
import {
|
||||
createChannelReplyPipeline,
|
||||
resolveInboundRouteEnvelopeBuilderWithRuntime,
|
||||
resolveWebhookPath,
|
||||
} from "../runtime-api.js";
|
||||
@@ -9,6 +8,7 @@ import { type ResolvedGoogleChatAccount } from "./accounts.js";
|
||||
import { downloadGoogleChatMedia, sendGoogleChatMessage } from "./api.js";
|
||||
import { type GoogleChatAudienceType } from "./auth.js";
|
||||
import { applyGoogleChatInboundAccessPolicy } from "./monitor-access.js";
|
||||
import { resolveGoogleChatDurableReplyOptions } from "./monitor-durable.js";
|
||||
import { deliverGoogleChatReply } from "./monitor-reply-delivery.js";
|
||||
import {
|
||||
registerGoogleChatWebhookTarget,
|
||||
@@ -281,13 +281,6 @@ async function processMessageWithPipeline(params: {
|
||||
}
|
||||
}
|
||||
|
||||
const { onModelSelected, ...replyPipeline } = createChannelReplyPipeline({
|
||||
cfg: config,
|
||||
agentId: route.agentId,
|
||||
channel: "googlechat",
|
||||
accountId: route.accountId,
|
||||
});
|
||||
|
||||
await core.channel.turn.run({
|
||||
channel: "googlechat",
|
||||
accountId: route.accountId,
|
||||
@@ -313,6 +306,13 @@ async function processMessageWithPipeline(params: {
|
||||
dispatchReplyWithBufferedBlockDispatcher:
|
||||
core.channel.reply.dispatchReplyWithBufferedBlockDispatcher,
|
||||
delivery: {
|
||||
durable: (payload, info) =>
|
||||
resolveGoogleChatDurableReplyOptions({
|
||||
payload,
|
||||
infoKind: info.kind,
|
||||
spaceId,
|
||||
typingMessageName,
|
||||
}),
|
||||
deliver: async (payload) => {
|
||||
await deliverGoogleChatReply({
|
||||
payload,
|
||||
@@ -327,16 +327,16 @@ async function processMessageWithPipeline(params: {
|
||||
// Only use typing message for first delivery
|
||||
typingMessageName = undefined;
|
||||
},
|
||||
onDelivered: () => {
|
||||
statusSink?.({ lastOutboundAt: Date.now() });
|
||||
},
|
||||
onError: (err, info) => {
|
||||
runtime.error?.(
|
||||
`[${account.accountId}] Google Chat ${info.kind} reply failed: ${String(err)}`,
|
||||
);
|
||||
},
|
||||
},
|
||||
dispatcherOptions: replyPipeline,
|
||||
replyOptions: {
|
||||
onModelSelected,
|
||||
},
|
||||
replyPipeline: {},
|
||||
record: {
|
||||
onRecordError: (err) => {
|
||||
runtime.error?.(`googlechat: failed updating session meta: ${String(err)}`);
|
||||
|
||||
Reference in New Issue
Block a user