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:
parent
d7ede3cd68
commit
149ad09832
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -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> {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user