fix(googlechat): fetch and include quoted message content in thread replies

- Add getGoogleChatMessage() API function to fetch messages by name
- Reorder message processing to fetch quoted content before body creation
- Include QuotedMessageText in agent context for thread replies
- Update documentation with implementation details

Fixes thread reply context for Google Chat
This commit is contained in:
root 2026-01-29 11:59:12 +00:00
parent d7ede3cd68
commit 149ad09832
2 changed files with 49 additions and 28 deletions

View File

@ -153,7 +153,7 @@ export async function uploadGoogleChatAttachment(params: {
contentType?: string; contentType?: string;
}): Promise<{ attachmentUploadToken?: string }> { }): Promise<{ attachmentUploadToken?: string }> {
const { account, space, filename, buffer, contentType } = params; const { account, space, filename, buffer, contentType } = params;
const boundary = `moltbot-${crypto.randomUUID()}`; const boundary = `clawdbot-${crypto.randomUUID()}`;
const metadata = JSON.stringify({ filename }); const metadata = JSON.stringify({ filename });
const header = `--${boundary}\r\nContent-Type: application/json; charset=UTF-8\r\n\r\n${metadata}\r\n`; const header = `--${boundary}\r\nContent-Type: application/json; charset=UTF-8\r\n\r\n${metadata}\r\n`;
const mediaHeader = `--${boundary}\r\nContent-Type: ${contentType ?? "application/octet-stream"}\r\n\r\n`; const mediaHeader = `--${boundary}\r\nContent-Type: ${contentType ?? "application/octet-stream"}\r\n\r\n`;
@ -257,3 +257,20 @@ export async function probeGoogleChat(account: ResolvedGoogleChatAccount): Promi
return { ok: false, error: err instanceof Error ? err.message : String(err) }; return { ok: false, error: err instanceof Error ? err.message : String(err) };
} }
} }
export async function getGoogleChatMessage(params: {
account: ResolvedGoogleChatAccount;
messageName: string;
}): Promise<{ text?: string; name?: string; thread?: { name?: string } } | null> {
const { account, messageName } = params;
const url = `${CHAT_API_BASE}/${messageName}`;
try {
return await fetchJson<{ text?: string; name?: string; thread?: { name?: string } }>(
account,
url,
{ method: "GET" }
);
} catch (err) {
return null;
}
}

View File

@ -1,6 +1,6 @@
import type { IncomingMessage, ServerResponse } from "node:http"; import type { IncomingMessage, ServerResponse } from "node:http";
import type { MoltbotConfig } from "clawdbot/plugin-sdk"; import type { ClawdbotConfig } from "clawdbot/plugin-sdk";
import { resolveMentionGatingWithBypass } from "clawdbot/plugin-sdk"; import { resolveMentionGatingWithBypass } from "clawdbot/plugin-sdk";
import { import {
@ -11,10 +11,10 @@ import {
deleteGoogleChatMessage, deleteGoogleChatMessage,
sendGoogleChatMessage, sendGoogleChatMessage,
updateGoogleChatMessage, updateGoogleChatMessage,
getGoogleChatMessage,
} from "./api.js"; } from "./api.js";
import { verifyGoogleChatRequest, type GoogleChatAudienceType } from "./auth.js"; import { verifyGoogleChatRequest, type GoogleChatAudienceType } from "./auth.js";
import { getGoogleChatRuntime } from "./runtime.js"; import { getGoogleChatRuntime } from "./runtime.js";
import { extractSpaceInfoFromEvent, buildSpaceCachePatch } from "./space-cache.js";
import type { import type {
GoogleChatAnnotation, GoogleChatAnnotation,
GoogleChatAttachment, GoogleChatAttachment,
@ -31,7 +31,7 @@ export type GoogleChatRuntimeEnv = {
export type GoogleChatMonitorOptions = { export type GoogleChatMonitorOptions = {
account: ResolvedGoogleChatAccount; account: ResolvedGoogleChatAccount;
config: MoltbotConfig; config: ClawdbotConfig;
runtime: GoogleChatRuntimeEnv; runtime: GoogleChatRuntimeEnv;
abortSignal: AbortSignal; abortSignal: AbortSignal;
webhookPath?: string; webhookPath?: string;
@ -43,7 +43,7 @@ type GoogleChatCoreRuntime = ReturnType<typeof getGoogleChatRuntime>;
type WebhookTarget = { type WebhookTarget = {
account: ResolvedGoogleChatAccount; account: ResolvedGoogleChatAccount;
config: MoltbotConfig; config: ClawdbotConfig;
runtime: GoogleChatRuntimeEnv; runtime: GoogleChatRuntimeEnv;
core: GoogleChatCoreRuntime; core: GoogleChatCoreRuntime;
path: string; path: string;
@ -265,7 +265,7 @@ export async function handleGoogleChatWebhookRequest(
selected.statusSink?.({ lastInboundAt: Date.now() }); selected.statusSink?.({ lastInboundAt: Date.now() });
// For synchronous responses in spaces, handle non-MESSAGE events immediately // For synchronous responses in spaces, we need to return a proper message
const evtType = (event.type ?? (event as { eventType?: string }).eventType)?.toUpperCase(); const evtType = (event.type ?? (event as { eventType?: string }).eventType)?.toUpperCase();
const isGroup = event.space?.type?.toUpperCase() !== "DM"; const isGroup = event.space?.type?.toUpperCase() !== "DM";
@ -273,7 +273,7 @@ export async function handleGoogleChatWebhookRequest(
if (isGroup && evtType !== "MESSAGE") { if (isGroup && evtType !== "MESSAGE") {
res.statusCode = 200; res.statusCode = 200;
res.setHeader("Content-Type", "application/json"); res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify({ text: "Hello! I'm ready to help. 🦞" })); res.end(JSON.stringify({ text: "Hello! I'm Chopper! 🦌" }));
return true; return true;
} }
@ -371,24 +371,24 @@ function extractMentionInfo(annotations: GoogleChatAnnotation[], botUser?: strin
* Resolve bot display name with fallback chain: * Resolve bot display name with fallback chain:
* 1. Account config name * 1. Account config name
* 2. Agent name from config * 2. Agent name from config
* 3. "Moltbot" as generic fallback * 3. "Clawdbot" as generic fallback
*/ */
function resolveBotDisplayName(params: { function resolveBotDisplayName(params: {
accountName?: string; accountName?: string;
agentId: string; agentId: string;
config: MoltbotConfig; config: ClawdbotConfig;
}): string { }): string {
const { accountName, agentId, config } = params; const { accountName, agentId, config } = params;
if (accountName?.trim()) return accountName.trim(); if (accountName?.trim()) return accountName.trim();
const agent = config.agents?.list?.find((a) => a.id === agentId); const agent = config.agents?.list?.find((a) => a.id === agentId);
if (agent?.name?.trim()) return agent.name.trim(); if (agent?.name?.trim()) return agent.name.trim();
return "Moltbot"; return "Clawdbot";
} }
async function processMessageWithPipeline(params: { async function processMessageWithPipeline(params: {
event: GoogleChatEvent; event: GoogleChatEvent;
account: ResolvedGoogleChatAccount; account: ResolvedGoogleChatAccount;
config: MoltbotConfig; config: ClawdbotConfig;
runtime: GoogleChatRuntimeEnv; runtime: GoogleChatRuntimeEnv;
core: GoogleChatCoreRuntime; core: GoogleChatCoreRuntime;
statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void; statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void;
@ -408,18 +408,6 @@ async function processMessageWithPipeline(params: {
const senderName = sender?.displayName ?? ""; const senderName = sender?.displayName ?? "";
const senderEmail = sender?.email ?? undefined; const senderEmail = sender?.email ?? undefined;
// Cache space mapping for proactive messaging
if (senderId && spaceId) {
const spaceInfo = extractSpaceInfoFromEvent(event);
if (spaceInfo) {
const cachePatch = buildSpaceCachePatch(spaceInfo, account.accountId);
core.config.patchConfig(cachePatch).catch((err: Error) => {
logVerbose(core, runtime, `failed to cache space: ${err.message}`);
});
logVerbose(core, runtime, `cached space ${spaceId} for user ${senderId}`);
}
}
const allowBots = account.config.allowBots === true; const allowBots = account.config.allowBots === true;
if (!allowBots) { if (!allowBots) {
if (sender?.type?.toUpperCase() === "BOT") { if (sender?.type?.toUpperCase() === "BOT") {
@ -435,7 +423,7 @@ async function processMessageWithPipeline(params: {
const messageText = (message.argumentText ?? message.text ?? "").trim(); const messageText = (message.argumentText ?? message.text ?? "").trim();
const attachments = message.attachment ?? []; const attachments = message.attachment ?? [];
const hasMedia = attachments.length > 0; const hasMedia = attachments.length > 0;
const rawBody = messageText || (hasMedia ? "<media:attachment>" : ""); let rawBody = messageText || (hasMedia ? "<media:attachment>" : "");
if (!rawBody) return; if (!rawBody) return;
const defaultGroupPolicy = config.channels?.defaults?.groupPolicy; const defaultGroupPolicy = config.channels?.defaults?.groupPolicy;
@ -612,6 +600,21 @@ async function processMessageWithPipeline(params: {
agentId: route.agentId, agentId: route.agentId,
}); });
const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(config); const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(config);
// Fetch quoted message content BEFORE creating body
let quotedMessageText: string | undefined;
const quotedName = message.quotedMessageMetadata?.name;
if (quotedName) {
try {
const quotedMsg = await getGoogleChatMessage({
account,
messageName: quotedName,
});
quotedMessageText = quotedMsg?.text;
} catch {
// Ignore fetch errors
}
}
const previousTimestamp = core.channel.session.readSessionUpdatedAt({ const previousTimestamp = core.channel.session.readSessionUpdatedAt({
storePath, storePath,
sessionKey: route.sessionKey, sessionKey: route.sessionKey,
@ -658,6 +661,7 @@ async function processMessageWithPipeline(params: {
// Thread reply context // Thread reply context
IsThreadReply: message.threadReply, IsThreadReply: message.threadReply,
QuotedMessageId: message.quotedMessageMetadata?.name, QuotedMessageId: message.quotedMessageMetadata?.name,
QuotedMessageText: quotedMessageText,
}); });
void core.channel.session void core.channel.session
@ -755,7 +759,7 @@ async function deliverGoogleChatReply(params: {
spaceId: string; spaceId: string;
runtime: GoogleChatRuntimeEnv; runtime: GoogleChatRuntimeEnv;
core: GoogleChatCoreRuntime; core: GoogleChatCoreRuntime;
config: MoltbotConfig; config: ClawdbotConfig;
statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void; statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void;
typingMessageName?: string; typingMessageName?: string;
}): Promise<void> { }): Promise<void> {