





















@@ -0,0 +1,156 @@
1+import {
2+deliverTextOrMediaReply,
3+resolveSendableOutboundReplyParts,
4+} from "openclaw/plugin-sdk/reply-payload";
5+import type { OpenClawConfig } from "../runtime-api.js";
6+import type { ResolvedGoogleChatAccount } from "./accounts.js";
7+import {
8+deleteGoogleChatMessage,
9+sendGoogleChatMessage,
10+updateGoogleChatMessage,
11+uploadGoogleChatAttachment,
12+} from "./api.js";
13+import type { GoogleChatCoreRuntime, GoogleChatRuntimeEnv } from "./monitor-types.js";
14+15+export async function deliverGoogleChatReply(params: {
16+payload: { text?: string; mediaUrls?: string[]; mediaUrl?: string; replyToId?: string };
17+account: ResolvedGoogleChatAccount;
18+spaceId: string;
19+runtime: GoogleChatRuntimeEnv;
20+core: GoogleChatCoreRuntime;
21+config: OpenClawConfig;
22+statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void;
23+typingMessageName?: string;
24+}): Promise<void> {
25+const { payload, account, spaceId, runtime, core, config, statusSink } = params;
26+// Clear this whenever the typing message is deleted or unavailable; otherwise
27+// text delivery can keep retrying a dead message and drop content.
28+let typingMessageName = params.typingMessageName;
29+const reply = resolveSendableOutboundReplyParts(payload);
30+const mediaCount = reply.mediaCount;
31+const hasMedia = reply.hasMedia;
32+const text = reply.text;
33+let firstTextChunk = true;
34+let suppressCaption = false;
35+36+if (hasMedia && typingMessageName) {
37+try {
38+await deleteGoogleChatMessage({
39+ account,
40+messageName: typingMessageName,
41+});
42+typingMessageName = undefined;
43+} catch (err) {
44+runtime.error?.(`Google Chat typing cleanup failed: ${String(err)}`);
45+if (typingMessageName) {
46+const fallbackText = reply.hasText
47+ ? text
48+ : mediaCount > 1
49+ ? "Sent attachments."
50+ : "Sent attachment.";
51+try {
52+await updateGoogleChatMessage({
53+ account,
54+messageName: typingMessageName,
55+text: fallbackText,
56+});
57+suppressCaption = Boolean(text.trim());
58+} catch (updateErr) {
59+runtime.error?.(`Google Chat typing update failed: ${String(updateErr)}`);
60+typingMessageName = undefined;
61+}
62+}
63+}
64+}
65+66+const chunkLimit = account.config.textChunkLimit ?? 4000;
67+const chunkMode = core.channel.text.resolveChunkMode(config, "googlechat", account.accountId);
68+const sendTextMessage = async (chunk: string) => {
69+await sendGoogleChatMessage({
70+ account,
71+space: spaceId,
72+text: chunk,
73+thread: payload.replyToId,
74+});
75+};
76+await deliverTextOrMediaReply({
77+ payload,
78+text: suppressCaption ? "" : reply.text,
79+chunkText: (value) => core.channel.text.chunkMarkdownTextWithMode(value, chunkLimit, chunkMode),
80+sendText: async (chunk) => {
81+try {
82+if (firstTextChunk && typingMessageName) {
83+await updateGoogleChatMessage({
84+ account,
85+messageName: typingMessageName,
86+text: chunk,
87+});
88+} else {
89+await sendTextMessage(chunk);
90+}
91+firstTextChunk = false;
92+statusSink?.({ lastOutboundAt: Date.now() });
93+} catch (err) {
94+runtime.error?.(`Google Chat message send failed: ${String(err)}`);
95+if (firstTextChunk && typingMessageName) {
96+typingMessageName = undefined;
97+try {
98+await sendTextMessage(chunk);
99+statusSink?.({ lastOutboundAt: Date.now() });
100+} catch (fallbackErr) {
101+runtime.error?.(`Google Chat message fallback send failed: ${String(fallbackErr)}`);
102+} finally {
103+firstTextChunk = false;
104+}
105+}
106+}
107+},
108+sendMedia: async ({ mediaUrl, caption }) => {
109+try {
110+const loaded = await core.channel.media.fetchRemoteMedia({
111+url: mediaUrl,
112+maxBytes: (account.config.mediaMaxMb ?? 20) * 1024 * 1024,
113+});
114+const upload = await uploadAttachmentForReply({
115+ account,
116+ spaceId,
117+buffer: loaded.buffer,
118+contentType: loaded.contentType,
119+filename: loaded.fileName ?? "attachment",
120+});
121+if (!upload.attachmentUploadToken) {
122+throw new Error("missing attachment upload token");
123+}
124+await sendGoogleChatMessage({
125+ account,
126+space: spaceId,
127+text: caption,
128+thread: payload.replyToId,
129+attachments: [
130+{ attachmentUploadToken: upload.attachmentUploadToken, contentName: loaded.fileName },
131+],
132+});
133+statusSink?.({ lastOutboundAt: Date.now() });
134+} catch (err) {
135+runtime.error?.(`Google Chat attachment send failed: ${String(err)}`);
136+}
137+},
138+});
139+}
140+141+async function uploadAttachmentForReply(params: {
142+account: ResolvedGoogleChatAccount;
143+spaceId: string;
144+buffer: Buffer;
145+contentType?: string;
146+filename: string;
147+}) {
148+const { account, spaceId, buffer, contentType, filename } = params;
149+return await uploadGoogleChatAttachment({
150+ account,
151+space: spaceId,
152+ filename,
153+ buffer,
154+ contentType,
155+});
156+}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。