@kodelyth/googlechat 2026.5.39 → 2026.5.42
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/api.ts +3 -0
- package/channel-config-api.ts +1 -0
- package/channel-plugin-api.ts +1 -0
- package/config-api.ts +2 -0
- package/contract-api.ts +5 -0
- package/dist/actions-YK1wn4ed.js +160 -0
- package/dist/api-BkZX4VNX.js +633 -0
- package/dist/api.js +3 -0
- package/dist/channel-DFZdjXD6.js +584 -0
- package/dist/channel-config-api.js +6 -0
- package/dist/channel-plugin-api.js +2 -0
- package/dist/channel.runtime-en3RNg9S.js +998 -0
- package/dist/contract-api.js +3 -0
- package/dist/doctor-contract-8SF6XoKj.js +151 -0
- package/dist/doctor-contract-api.js +2 -0
- package/dist/index.js +22 -0
- package/dist/runtime-api-DUH2Cg-0.js +29 -0
- package/dist/runtime-api.js +2 -0
- package/dist/secret-contract-DWX4ikgT.js +99 -0
- package/dist/secret-contract-api.js +2 -0
- package/dist/setup-entry.js +15 -0
- package/dist/setup-plugin-api.js +75 -0
- package/dist/setup-surface-B3Fa7XRx.js +321 -0
- package/dist/test-api.js +3 -0
- package/doctor-contract-api.ts +1 -0
- package/index.ts +20 -0
- package/klaw.plugin.json +2 -967
- package/package.json +4 -4
- package/runtime-api.ts +55 -0
- package/secret-contract-api.ts +5 -0
- package/setup-entry.ts +13 -0
- package/setup-plugin-api.ts +3 -0
- package/src/accounts.ts +181 -0
- package/src/actions.test.ts +289 -0
- package/src/actions.ts +227 -0
- package/src/api.ts +316 -0
- package/src/approval-auth.test.ts +24 -0
- package/src/approval-auth.ts +32 -0
- package/src/auth.ts +218 -0
- package/src/channel-config.test.ts +39 -0
- package/src/channel.adapters.ts +340 -0
- package/src/channel.deps.runtime.ts +29 -0
- package/src/channel.runtime.ts +17 -0
- package/src/channel.setup.ts +98 -0
- package/src/channel.test.ts +784 -0
- package/src/channel.ts +277 -0
- package/src/config-schema.test.ts +31 -0
- package/src/config-schema.ts +3 -0
- package/src/doctor-contract.test.ts +75 -0
- package/src/doctor-contract.ts +182 -0
- package/src/doctor.ts +57 -0
- package/src/gateway.ts +63 -0
- package/src/google-auth.runtime.test.ts +543 -0
- package/src/google-auth.runtime.ts +568 -0
- package/src/group-policy.ts +17 -0
- package/src/monitor-access.test.ts +491 -0
- package/src/monitor-access.ts +465 -0
- package/src/monitor-durable.test.ts +39 -0
- package/src/monitor-durable.ts +23 -0
- package/src/monitor-reply-delivery.ts +156 -0
- package/src/monitor-routing.ts +65 -0
- package/src/monitor-types.ts +33 -0
- package/src/monitor-webhook.test.ts +587 -0
- package/src/monitor-webhook.ts +303 -0
- package/src/monitor.reply-delivery.test.ts +144 -0
- package/src/monitor.test.ts +159 -0
- package/src/monitor.ts +527 -0
- package/src/monitor.webhook-routing.test.ts +257 -0
- package/src/runtime.ts +9 -0
- package/src/secret-contract.test.ts +60 -0
- package/src/secret-contract.ts +161 -0
- package/src/setup-core.ts +40 -0
- package/src/setup-surface.ts +243 -0
- package/src/setup.test.ts +619 -0
- package/src/targets.test.ts +453 -0
- package/src/targets.ts +66 -0
- package/src/types.config.ts +3 -0
- package/src/types.ts +73 -0
- package/test-api.ts +2 -0
- package/tsconfig.json +16 -0
- package/api.js +0 -7
- package/channel-config-api.js +0 -7
- package/channel-plugin-api.js +0 -7
- package/contract-api.js +0 -7
- package/doctor-contract-api.js +0 -7
- package/index.js +0 -7
- package/runtime-api.js +0 -7
- package/secret-contract-api.js +0 -7
- package/setup-entry.js +0 -7
- package/setup-plugin-api.js +0 -7
- package/test-api.js +0 -7
package/src/monitor.ts
ADDED
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
import {
|
|
2
|
+
recordChannelBotPairLoopAndCheckSuppression,
|
|
3
|
+
type ChannelBotLoopProtectionFacts,
|
|
4
|
+
} from "klaw/plugin-sdk/inbound-reply-dispatch";
|
|
5
|
+
import { mergePairLoopGuardConfig } from "klaw/plugin-sdk/pair-loop-guard-runtime";
|
|
6
|
+
import { normalizeOptionalLowercaseString } from "klaw/plugin-sdk/string-coerce-runtime";
|
|
7
|
+
import type { KlawConfig } from "../runtime-api.js";
|
|
8
|
+
import {
|
|
9
|
+
resolveInboundRouteEnvelopeBuilderWithRuntime,
|
|
10
|
+
resolveWebhookPath,
|
|
11
|
+
} from "../runtime-api.js";
|
|
12
|
+
import { type ResolvedGoogleChatAccount } from "./accounts.js";
|
|
13
|
+
import { downloadGoogleChatMedia, sendGoogleChatMessage } from "./api.js";
|
|
14
|
+
import { type GoogleChatAudienceType } from "./auth.js";
|
|
15
|
+
import { applyGoogleChatInboundAccessPolicy } from "./monitor-access.js";
|
|
16
|
+
import { resolveGoogleChatDurableReplyOptions } from "./monitor-durable.js";
|
|
17
|
+
import { deliverGoogleChatReply } from "./monitor-reply-delivery.js";
|
|
18
|
+
import {
|
|
19
|
+
registerGoogleChatWebhookTarget,
|
|
20
|
+
setGoogleChatWebhookEventProcessor,
|
|
21
|
+
} from "./monitor-routing.js";
|
|
22
|
+
import type {
|
|
23
|
+
GoogleChatCoreRuntime,
|
|
24
|
+
GoogleChatMonitorOptions,
|
|
25
|
+
GoogleChatRuntimeEnv,
|
|
26
|
+
WebhookTarget,
|
|
27
|
+
} from "./monitor-types.js";
|
|
28
|
+
import { warnAppPrincipalMisconfiguration } from "./monitor-webhook.js";
|
|
29
|
+
import { getGoogleChatRuntime } from "./runtime.js";
|
|
30
|
+
import type { GoogleChatAttachment, GoogleChatEvent } from "./types.js";
|
|
31
|
+
|
|
32
|
+
setGoogleChatWebhookEventProcessor(processGoogleChatEvent);
|
|
33
|
+
|
|
34
|
+
function logVerbose(core: GoogleChatCoreRuntime, runtime: GoogleChatRuntimeEnv, message: string) {
|
|
35
|
+
if (core.logging.shouldLogVerbose()) {
|
|
36
|
+
runtime.log?.(`[googlechat] ${message}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function normalizeAudienceType(value?: string | null): GoogleChatAudienceType | undefined {
|
|
41
|
+
const normalized = normalizeOptionalLowercaseString(value);
|
|
42
|
+
if (normalized === "app-url" || normalized === "app_url" || normalized === "app") {
|
|
43
|
+
return "app-url";
|
|
44
|
+
}
|
|
45
|
+
if (
|
|
46
|
+
normalized === "project-number" ||
|
|
47
|
+
normalized === "project_number" ||
|
|
48
|
+
normalized === "project"
|
|
49
|
+
) {
|
|
50
|
+
return "project-number";
|
|
51
|
+
}
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function resolveGoogleChatTimestampMs(eventTime?: string): number | undefined {
|
|
56
|
+
if (!eventTime) {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
const parsed = Date.parse(eventTime);
|
|
60
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function resolveGoogleChatBotLoopProtection(params: {
|
|
64
|
+
allowBots: boolean;
|
|
65
|
+
isBotSender: boolean;
|
|
66
|
+
senderId: string;
|
|
67
|
+
appUserId: string;
|
|
68
|
+
accountId: string;
|
|
69
|
+
conversationId: string;
|
|
70
|
+
config?: ChannelBotLoopProtectionFacts["config"];
|
|
71
|
+
defaultsConfig?: ChannelBotLoopProtectionFacts["defaultsConfig"];
|
|
72
|
+
eventTime?: string;
|
|
73
|
+
}): ChannelBotLoopProtectionFacts | undefined {
|
|
74
|
+
if (
|
|
75
|
+
!params.allowBots ||
|
|
76
|
+
!params.isBotSender ||
|
|
77
|
+
!params.senderId ||
|
|
78
|
+
params.senderId === params.appUserId
|
|
79
|
+
) {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
scopeId: params.accountId,
|
|
84
|
+
conversationId: params.conversationId,
|
|
85
|
+
senderId: params.senderId,
|
|
86
|
+
receiverId: params.appUserId,
|
|
87
|
+
config: params.config,
|
|
88
|
+
defaultsConfig: params.defaultsConfig,
|
|
89
|
+
defaultEnabled: true,
|
|
90
|
+
nowMs: resolveGoogleChatTimestampMs(params.eventTime),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function resolveGoogleChatBotLoopProtectionConfig(params: {
|
|
95
|
+
accountConfig?: ChannelBotLoopProtectionFacts["config"];
|
|
96
|
+
groupConfig?: ChannelBotLoopProtectionFacts["config"];
|
|
97
|
+
}): ChannelBotLoopProtectionFacts["config"] {
|
|
98
|
+
return mergePairLoopGuardConfig(params.accountConfig, params.groupConfig);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function shouldSuppressGoogleChatBotLoop(params: {
|
|
102
|
+
botLoopProtection?: ChannelBotLoopProtectionFacts;
|
|
103
|
+
core: GoogleChatCoreRuntime;
|
|
104
|
+
runtime: GoogleChatRuntimeEnv;
|
|
105
|
+
}): boolean {
|
|
106
|
+
if (!params.botLoopProtection) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
const botLoopResult = recordChannelBotPairLoopAndCheckSuppression(params.botLoopProtection);
|
|
110
|
+
if (!botLoopResult.suppressed) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
logVerbose(
|
|
114
|
+
params.core,
|
|
115
|
+
params.runtime,
|
|
116
|
+
`skip bot-to-bot loop in ${params.botLoopProtection.conversationId}`,
|
|
117
|
+
);
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function processGoogleChatEvent(event: GoogleChatEvent, target: WebhookTarget) {
|
|
122
|
+
const eventType = event.type ?? (event as { eventType?: string }).eventType;
|
|
123
|
+
if (eventType !== "MESSAGE") {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (!event.message || !event.space) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
await processMessageWithPipeline({
|
|
131
|
+
event,
|
|
132
|
+
account: target.account,
|
|
133
|
+
config: target.config,
|
|
134
|
+
runtime: target.runtime,
|
|
135
|
+
core: target.core,
|
|
136
|
+
statusSink: target.statusSink,
|
|
137
|
+
mediaMaxMb: target.mediaMaxMb,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Resolve bot display name with fallback chain:
|
|
143
|
+
* 1. Account config name
|
|
144
|
+
* 2. Agent name from config
|
|
145
|
+
* 3. "Klaw" as generic fallback
|
|
146
|
+
*/
|
|
147
|
+
function resolveBotDisplayName(params: {
|
|
148
|
+
accountName?: string;
|
|
149
|
+
agentId: string;
|
|
150
|
+
config: KlawConfig;
|
|
151
|
+
}): string {
|
|
152
|
+
const { accountName, agentId, config } = params;
|
|
153
|
+
if (accountName?.trim()) {
|
|
154
|
+
return accountName.trim();
|
|
155
|
+
}
|
|
156
|
+
const agent = config.agents?.list?.find((a) => a.id === agentId);
|
|
157
|
+
if (agent?.name?.trim()) {
|
|
158
|
+
return agent.name.trim();
|
|
159
|
+
}
|
|
160
|
+
return "Klaw";
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function processMessageWithPipeline(params: {
|
|
164
|
+
event: GoogleChatEvent;
|
|
165
|
+
account: ResolvedGoogleChatAccount;
|
|
166
|
+
config: KlawConfig;
|
|
167
|
+
runtime: GoogleChatRuntimeEnv;
|
|
168
|
+
core: GoogleChatCoreRuntime;
|
|
169
|
+
statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void;
|
|
170
|
+
mediaMaxMb: number;
|
|
171
|
+
}): Promise<void> {
|
|
172
|
+
const { event, account, config, runtime, core, statusSink, mediaMaxMb } = params;
|
|
173
|
+
const space = event.space;
|
|
174
|
+
const message = event.message;
|
|
175
|
+
if (!space || !message) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const spaceId = space.name ?? "";
|
|
180
|
+
if (!spaceId) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const spaceType = (space.type ?? "").toUpperCase();
|
|
184
|
+
const isGroup = spaceType !== "DM";
|
|
185
|
+
const sender = message.sender ?? event.user;
|
|
186
|
+
const senderId = sender?.name ?? "";
|
|
187
|
+
const senderName = sender?.displayName ?? "";
|
|
188
|
+
const senderEmail = sender?.email ?? undefined;
|
|
189
|
+
const isBotSender = sender?.type?.toUpperCase() === "BOT";
|
|
190
|
+
const appUserId = account.config.botUser?.trim() || "users/app";
|
|
191
|
+
|
|
192
|
+
const allowBots = account.config.allowBots === true;
|
|
193
|
+
if (!allowBots) {
|
|
194
|
+
if (isBotSender) {
|
|
195
|
+
logVerbose(core, runtime, `skip bot-authored message (${senderId || "unknown"})`);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (senderId === "users/app") {
|
|
199
|
+
logVerbose(core, runtime, "skip app-authored message");
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const messageText = (message.argumentText ?? message.text ?? "").trim();
|
|
205
|
+
const attachments = message.attachment ?? [];
|
|
206
|
+
const hasMedia = attachments.length > 0;
|
|
207
|
+
const rawBody = messageText || (hasMedia ? "<media:attachment>" : "");
|
|
208
|
+
if (!rawBody) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const access = await applyGoogleChatInboundAccessPolicy({
|
|
213
|
+
account,
|
|
214
|
+
config,
|
|
215
|
+
core,
|
|
216
|
+
space,
|
|
217
|
+
message,
|
|
218
|
+
isGroup,
|
|
219
|
+
senderId,
|
|
220
|
+
senderName,
|
|
221
|
+
senderEmail,
|
|
222
|
+
rawBody,
|
|
223
|
+
statusSink,
|
|
224
|
+
logVerbose: (message) => logVerbose(core, runtime, message),
|
|
225
|
+
});
|
|
226
|
+
if (!access.ok) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const { commandAuthorized, effectiveWasMentioned, groupBotLoopProtection, groupSystemPrompt } =
|
|
230
|
+
access;
|
|
231
|
+
const botLoopProtection = resolveGoogleChatBotLoopProtection({
|
|
232
|
+
allowBots,
|
|
233
|
+
isBotSender,
|
|
234
|
+
senderId,
|
|
235
|
+
appUserId,
|
|
236
|
+
accountId: account.accountId,
|
|
237
|
+
conversationId: spaceId,
|
|
238
|
+
config: resolveGoogleChatBotLoopProtectionConfig({
|
|
239
|
+
accountConfig: account.config.botLoopProtection,
|
|
240
|
+
groupConfig: groupBotLoopProtection,
|
|
241
|
+
}),
|
|
242
|
+
defaultsConfig: config.channels?.defaults?.botLoopProtection,
|
|
243
|
+
eventTime: event.eventTime,
|
|
244
|
+
});
|
|
245
|
+
if (shouldSuppressGoogleChatBotLoop({ botLoopProtection, core, runtime })) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const { route, buildEnvelope } = resolveInboundRouteEnvelopeBuilderWithRuntime({
|
|
250
|
+
cfg: config,
|
|
251
|
+
channel: "googlechat",
|
|
252
|
+
accountId: account.accountId,
|
|
253
|
+
peer: {
|
|
254
|
+
kind: isGroup ? ("group" as const) : ("direct" as const),
|
|
255
|
+
id: spaceId,
|
|
256
|
+
},
|
|
257
|
+
runtime: core.channel,
|
|
258
|
+
sessionStore: config.session?.store,
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
let mediaPath: string | undefined;
|
|
262
|
+
let mediaType: string | undefined;
|
|
263
|
+
if (attachments.length > 0) {
|
|
264
|
+
const first = attachments[0];
|
|
265
|
+
const attachmentData = await downloadAttachment(first, account, mediaMaxMb, core);
|
|
266
|
+
if (attachmentData) {
|
|
267
|
+
mediaPath = attachmentData.path;
|
|
268
|
+
mediaType = attachmentData.contentType;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const fromLabel = isGroup
|
|
273
|
+
? space.displayName || `space:${spaceId}`
|
|
274
|
+
: senderName || `user:${senderId}`;
|
|
275
|
+
const { storePath, body } = buildEnvelope({
|
|
276
|
+
channel: "Google Chat",
|
|
277
|
+
from: fromLabel,
|
|
278
|
+
timestamp: event.eventTime ? Date.parse(event.eventTime) : undefined,
|
|
279
|
+
body: rawBody,
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
const ctxPayload = core.channel.turn.buildContext({
|
|
283
|
+
channel: "googlechat",
|
|
284
|
+
accountId: route.accountId,
|
|
285
|
+
messageId: message.name,
|
|
286
|
+
messageIdFull: message.name,
|
|
287
|
+
timestamp: event.eventTime ? Date.parse(event.eventTime) : undefined,
|
|
288
|
+
from: `googlechat:${senderId}`,
|
|
289
|
+
sender: {
|
|
290
|
+
id: senderId,
|
|
291
|
+
name: senderName || undefined,
|
|
292
|
+
username: senderEmail,
|
|
293
|
+
},
|
|
294
|
+
conversation: {
|
|
295
|
+
kind: isGroup ? "channel" : "direct",
|
|
296
|
+
id: spaceId,
|
|
297
|
+
label: fromLabel,
|
|
298
|
+
routePeer: {
|
|
299
|
+
kind: isGroup ? "group" : "direct",
|
|
300
|
+
id: spaceId,
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
route: {
|
|
304
|
+
agentId: route.agentId,
|
|
305
|
+
accountId: route.accountId,
|
|
306
|
+
routeSessionKey: route.sessionKey,
|
|
307
|
+
},
|
|
308
|
+
reply: {
|
|
309
|
+
to: `googlechat:${spaceId}`,
|
|
310
|
+
originatingTo: `googlechat:${spaceId}`,
|
|
311
|
+
replyToId: message.thread?.name,
|
|
312
|
+
replyToIdFull: message.thread?.name,
|
|
313
|
+
},
|
|
314
|
+
message: {
|
|
315
|
+
body,
|
|
316
|
+
bodyForAgent: rawBody,
|
|
317
|
+
rawBody,
|
|
318
|
+
commandBody: rawBody,
|
|
319
|
+
envelopeFrom: fromLabel,
|
|
320
|
+
},
|
|
321
|
+
media:
|
|
322
|
+
mediaPath || mediaType
|
|
323
|
+
? [
|
|
324
|
+
{
|
|
325
|
+
path: mediaPath,
|
|
326
|
+
url: mediaPath,
|
|
327
|
+
contentType: mediaType,
|
|
328
|
+
},
|
|
329
|
+
]
|
|
330
|
+
: undefined,
|
|
331
|
+
supplemental: {
|
|
332
|
+
groupSystemPrompt: isGroup ? groupSystemPrompt : undefined,
|
|
333
|
+
},
|
|
334
|
+
extra: {
|
|
335
|
+
ChatType: isGroup ? "channel" : "direct",
|
|
336
|
+
WasMentioned: isGroup ? effectiveWasMentioned : undefined,
|
|
337
|
+
CommandAuthorized: commandAuthorized,
|
|
338
|
+
GroupSubject: undefined,
|
|
339
|
+
GroupSpace: isGroup ? (space.displayName ?? undefined) : undefined,
|
|
340
|
+
},
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
// Typing indicator setup
|
|
344
|
+
// Note: Reaction mode requires user OAuth, not available with service account auth.
|
|
345
|
+
// If reaction is configured, we fall back to message mode with a warning.
|
|
346
|
+
let typingIndicator = account.config.typingIndicator ?? "message";
|
|
347
|
+
if (typingIndicator === "reaction") {
|
|
348
|
+
runtime.error?.(
|
|
349
|
+
`[${account.accountId}] typingIndicator="reaction" requires user OAuth (not supported with service account). Falling back to "message" mode.`,
|
|
350
|
+
);
|
|
351
|
+
typingIndicator = "message";
|
|
352
|
+
}
|
|
353
|
+
let typingMessageName: string | undefined;
|
|
354
|
+
|
|
355
|
+
// Start typing indicator (message mode only, reaction mode not supported with app auth)
|
|
356
|
+
if (typingIndicator === "message") {
|
|
357
|
+
try {
|
|
358
|
+
const botName = resolveBotDisplayName({
|
|
359
|
+
accountName: account.config.name,
|
|
360
|
+
agentId: route.agentId,
|
|
361
|
+
config,
|
|
362
|
+
});
|
|
363
|
+
const result = await sendGoogleChatMessage({
|
|
364
|
+
account,
|
|
365
|
+
space: spaceId,
|
|
366
|
+
text: `_${botName} is typing..._`,
|
|
367
|
+
thread: message.thread?.name,
|
|
368
|
+
});
|
|
369
|
+
typingMessageName = result?.messageName;
|
|
370
|
+
} catch (err) {
|
|
371
|
+
runtime.error?.(`Failed sending typing message: ${String(err)}`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
await core.channel.turn.run({
|
|
376
|
+
channel: "googlechat",
|
|
377
|
+
accountId: route.accountId,
|
|
378
|
+
raw: message,
|
|
379
|
+
adapter: {
|
|
380
|
+
ingest: () => ({
|
|
381
|
+
id: message.name ?? spaceId,
|
|
382
|
+
timestamp: event.eventTime ? Date.parse(event.eventTime) : undefined,
|
|
383
|
+
rawText: rawBody,
|
|
384
|
+
textForAgent: rawBody,
|
|
385
|
+
textForCommands: rawBody,
|
|
386
|
+
raw: message,
|
|
387
|
+
}),
|
|
388
|
+
resolveTurn: () => ({
|
|
389
|
+
cfg: config,
|
|
390
|
+
channel: "googlechat",
|
|
391
|
+
accountId: route.accountId,
|
|
392
|
+
agentId: route.agentId,
|
|
393
|
+
routeSessionKey: route.sessionKey,
|
|
394
|
+
storePath,
|
|
395
|
+
ctxPayload,
|
|
396
|
+
recordInboundSession: core.channel.session.recordInboundSession,
|
|
397
|
+
dispatchReplyWithBufferedBlockDispatcher:
|
|
398
|
+
core.channel.reply.dispatchReplyWithBufferedBlockDispatcher,
|
|
399
|
+
delivery: {
|
|
400
|
+
durable: (payload, info) =>
|
|
401
|
+
resolveGoogleChatDurableReplyOptions({
|
|
402
|
+
payload,
|
|
403
|
+
infoKind: info.kind,
|
|
404
|
+
spaceId,
|
|
405
|
+
typingMessageName,
|
|
406
|
+
}),
|
|
407
|
+
deliver: async (payload) => {
|
|
408
|
+
await deliverGoogleChatReply({
|
|
409
|
+
payload,
|
|
410
|
+
account,
|
|
411
|
+
spaceId,
|
|
412
|
+
runtime,
|
|
413
|
+
core,
|
|
414
|
+
config,
|
|
415
|
+
statusSink,
|
|
416
|
+
typingMessageName,
|
|
417
|
+
});
|
|
418
|
+
// Only use typing message for first delivery
|
|
419
|
+
typingMessageName = undefined;
|
|
420
|
+
},
|
|
421
|
+
onDelivered: () => {
|
|
422
|
+
statusSink?.({ lastOutboundAt: Date.now() });
|
|
423
|
+
},
|
|
424
|
+
onError: (err, info) => {
|
|
425
|
+
runtime.error?.(
|
|
426
|
+
`[${account.accountId}] Google Chat ${info.kind} reply failed: ${String(err)}`,
|
|
427
|
+
);
|
|
428
|
+
},
|
|
429
|
+
},
|
|
430
|
+
replyPipeline: {},
|
|
431
|
+
record: {
|
|
432
|
+
onRecordError: (err) => {
|
|
433
|
+
runtime.error?.(`googlechat: failed updating session meta: ${String(err)}`);
|
|
434
|
+
},
|
|
435
|
+
},
|
|
436
|
+
}),
|
|
437
|
+
},
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
export const testing = {
|
|
442
|
+
processMessageWithPipeline,
|
|
443
|
+
resolveGoogleChatBotLoopProtection,
|
|
444
|
+
resolveGoogleChatBotLoopProtectionConfig,
|
|
445
|
+
shouldSuppressGoogleChatBotLoop,
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
async function downloadAttachment(
|
|
449
|
+
attachment: GoogleChatAttachment,
|
|
450
|
+
account: ResolvedGoogleChatAccount,
|
|
451
|
+
mediaMaxMb: number,
|
|
452
|
+
core: GoogleChatCoreRuntime,
|
|
453
|
+
): Promise<{ path: string; contentType?: string } | null> {
|
|
454
|
+
const resourceName = attachment.attachmentDataRef?.resourceName;
|
|
455
|
+
if (!resourceName) {
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
const maxBytes = Math.max(1, mediaMaxMb) * 1024 * 1024;
|
|
459
|
+
const downloaded = await downloadGoogleChatMedia({ account, resourceName, maxBytes });
|
|
460
|
+
const saved = await core.channel.media.saveMediaBuffer(
|
|
461
|
+
downloaded.buffer,
|
|
462
|
+
downloaded.contentType ?? attachment.contentType,
|
|
463
|
+
"inbound",
|
|
464
|
+
maxBytes,
|
|
465
|
+
attachment.contentName,
|
|
466
|
+
);
|
|
467
|
+
return { path: saved.path, contentType: saved.contentType };
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function monitorGoogleChatProvider(options: GoogleChatMonitorOptions): () => void {
|
|
471
|
+
const core = getGoogleChatRuntime();
|
|
472
|
+
const webhookPath = resolveWebhookPath({
|
|
473
|
+
webhookPath: options.webhookPath,
|
|
474
|
+
webhookUrl: options.webhookUrl,
|
|
475
|
+
defaultPath: "/googlechat",
|
|
476
|
+
});
|
|
477
|
+
if (!webhookPath) {
|
|
478
|
+
options.runtime.error?.(`[${options.account.accountId}] invalid webhook path`);
|
|
479
|
+
return () => {};
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const audienceType = normalizeAudienceType(options.account.config.audienceType);
|
|
483
|
+
const audience = options.account.config.audience?.trim();
|
|
484
|
+
const mediaMaxMb = options.account.config.mediaMaxMb ?? 20;
|
|
485
|
+
|
|
486
|
+
warnAppPrincipalMisconfiguration({
|
|
487
|
+
accountId: options.account.accountId,
|
|
488
|
+
audienceType,
|
|
489
|
+
appPrincipal: options.account.config.appPrincipal,
|
|
490
|
+
log: options.runtime.log,
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
const unregisterTarget = registerGoogleChatWebhookTarget({
|
|
494
|
+
account: options.account,
|
|
495
|
+
config: options.config,
|
|
496
|
+
runtime: options.runtime,
|
|
497
|
+
core,
|
|
498
|
+
path: webhookPath,
|
|
499
|
+
audienceType,
|
|
500
|
+
audience,
|
|
501
|
+
statusSink: options.statusSink,
|
|
502
|
+
mediaMaxMb,
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
return () => {
|
|
506
|
+
unregisterTarget();
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
export async function startGoogleChatMonitor(
|
|
511
|
+
params: GoogleChatMonitorOptions,
|
|
512
|
+
): Promise<() => void> {
|
|
513
|
+
return monitorGoogleChatProvider(params);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
export function resolveGoogleChatWebhookPath(params: {
|
|
517
|
+
account: ResolvedGoogleChatAccount;
|
|
518
|
+
}): string {
|
|
519
|
+
return (
|
|
520
|
+
resolveWebhookPath({
|
|
521
|
+
webhookPath: params.account.config.webhookPath,
|
|
522
|
+
webhookUrl: params.account.config.webhookUrl,
|
|
523
|
+
defaultPath: "/googlechat",
|
|
524
|
+
}) ?? "/googlechat"
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
export { testing as __testing };
|