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;
}): Promise<{ attachmentUploadToken?: string }> {
const { account, space, filename, buffer, contentType } = params;
const boundary = `moltbot-${crypto.randomUUID()}`;
const boundary = `clawdbot-${crypto.randomUUID()}`;
const metadata = JSON.stringify({ filename });
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`;
@ -257,3 +257,20 @@ export async function probeGoogleChat(account: ResolvedGoogleChatAccount): Promi
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 { MoltbotConfig } from "clawdbot/plugin-sdk";
import type { ClawdbotConfig } from "clawdbot/plugin-sdk";
import { resolveMentionGatingWithBypass } from "clawdbot/plugin-sdk";
import {
@ -11,10 +11,10 @@ import {
deleteGoogleChatMessage,
sendGoogleChatMessage,
updateGoogleChatMessage,
getGoogleChatMessage,
} from "./api.js";
import { verifyGoogleChatRequest, type GoogleChatAudienceType } from "./auth.js";
import { getGoogleChatRuntime } from "./runtime.js";
import { extractSpaceInfoFromEvent, buildSpaceCachePatch } from "./space-cache.js";
import type {
GoogleChatAnnotation,
GoogleChatAttachment,
@ -31,7 +31,7 @@ export type GoogleChatRuntimeEnv = {
export type GoogleChatMonitorOptions = {
account: ResolvedGoogleChatAccount;
config: MoltbotConfig;
config: ClawdbotConfig;
runtime: GoogleChatRuntimeEnv;
abortSignal: AbortSignal;
webhookPath?: string;
@ -43,7 +43,7 @@ type GoogleChatCoreRuntime = ReturnType<typeof getGoogleChatRuntime>;
type WebhookTarget = {
account: ResolvedGoogleChatAccount;
config: MoltbotConfig;
config: ClawdbotConfig;
runtime: GoogleChatRuntimeEnv;
core: GoogleChatCoreRuntime;
path: string;
@ -264,19 +264,19 @@ export async function handleGoogleChatWebhookRequest(
}
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 isGroup = event.space?.type?.toUpperCase() !== "DM";
// For non-MESSAGE events in groups (like ADDED_TO_SPACE), return an acknowledgment
if (isGroup && evtType !== "MESSAGE") {
res.statusCode = 200;
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;
}
processGoogleChatEvent(event, selected).catch((err) => {
selected?.runtime.error?.(
`[${selected.account.accountId}] Google Chat webhook failed: ${String(err)}`,
@ -371,24 +371,24 @@ function extractMentionInfo(annotations: GoogleChatAnnotation[], botUser?: strin
* Resolve bot display name with fallback chain:
* 1. Account config name
* 2. Agent name from config
* 3. "Moltbot" as generic fallback
* 3. "Clawdbot" as generic fallback
*/
function resolveBotDisplayName(params: {
accountName?: string;
agentId: string;
config: MoltbotConfig;
config: ClawdbotConfig;
}): string {
const { accountName, agentId, config } = params;
if (accountName?.trim()) return accountName.trim();
const agent = config.agents?.list?.find((a) => a.id === agentId);
if (agent?.name?.trim()) return agent.name.trim();
return "Moltbot";
return "Clawdbot";
}
async function processMessageWithPipeline(params: {
event: GoogleChatEvent;
account: ResolvedGoogleChatAccount;
config: MoltbotConfig;
config: ClawdbotConfig;
runtime: GoogleChatRuntimeEnv;
core: GoogleChatCoreRuntime;
statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void;
@ -408,18 +408,6 @@ async function processMessageWithPipeline(params: {
const senderName = sender?.displayName ?? "";
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;
if (!allowBots) {
if (sender?.type?.toUpperCase() === "BOT") {
@ -435,7 +423,7 @@ async function processMessageWithPipeline(params: {
const messageText = (message.argumentText ?? message.text ?? "").trim();
const attachments = message.attachment ?? [];
const hasMedia = attachments.length > 0;
const rawBody = messageText || (hasMedia ? "<media:attachment>" : "");
let rawBody = messageText || (hasMedia ? "<media:attachment>" : "");
if (!rawBody) return;
const defaultGroupPolicy = config.channels?.defaults?.groupPolicy;
@ -612,6 +600,21 @@ async function processMessageWithPipeline(params: {
agentId: route.agentId,
});
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({
storePath,
sessionKey: route.sessionKey,
@ -658,6 +661,7 @@ async function processMessageWithPipeline(params: {
// Thread reply context
IsThreadReply: message.threadReply,
QuotedMessageId: message.quotedMessageMetadata?.name,
QuotedMessageText: quotedMessageText,
});
void core.channel.session
@ -755,7 +759,7 @@ async function deliverGoogleChatReply(params: {
spaceId: string;
runtime: GoogleChatRuntimeEnv;
core: GoogleChatCoreRuntime;
config: MoltbotConfig;
config: ClawdbotConfig;
statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void;
typingMessageName?: string;
}): Promise<void> {