feat: improve BlueBubbles message processing by adding reply context formatting and enhancing message ID extraction from responses
This commit is contained in:
committed by
Peter Steinberger
parent
e5514d4854
commit
b0b42b4e14
@@ -1078,6 +1078,8 @@ describe("BlueBubbles webhook monitor", () => {
|
|||||||
expect(callArgs.ctx.ReplyToId).toBe("msg-0");
|
expect(callArgs.ctx.ReplyToId).toBe("msg-0");
|
||||||
expect(callArgs.ctx.ReplyToBody).toBe("original message");
|
expect(callArgs.ctx.ReplyToBody).toBe("original message");
|
||||||
expect(callArgs.ctx.ReplyToSender).toBe("+15550000000");
|
expect(callArgs.ctx.ReplyToSender).toBe("+15550000000");
|
||||||
|
expect(callArgs.ctx.Body).toContain("[Replying to +15550000000 id:msg-0]");
|
||||||
|
expect(callArgs.ctx.Body).toContain("original message");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -216,6 +216,21 @@ function buildMessagePlaceholder(message: NormalizedWebhookMessage): string {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatReplyContext(message: {
|
||||||
|
replyToId?: string;
|
||||||
|
replyToBody?: string;
|
||||||
|
replyToSender?: string;
|
||||||
|
}): string | null {
|
||||||
|
if (!message.replyToId && !message.replyToBody && !message.replyToSender) return null;
|
||||||
|
const sender = message.replyToSender?.trim() || "unknown sender";
|
||||||
|
const idPart = message.replyToId ? ` id:${message.replyToId}` : "";
|
||||||
|
const body = message.replyToBody?.trim();
|
||||||
|
if (!body) {
|
||||||
|
return `[Replying to ${sender}${idPart}]\n[/Replying]`;
|
||||||
|
}
|
||||||
|
return `[Replying to ${sender}${idPart}]\n${body}\n[/Replying]`;
|
||||||
|
}
|
||||||
|
|
||||||
function readNumberLike(record: Record<string, unknown> | null, key: string): number | undefined {
|
function readNumberLike(record: Record<string, unknown> | null, key: string): number | undefined {
|
||||||
if (!record) return undefined;
|
if (!record) return undefined;
|
||||||
const value = record[key];
|
const value = record[key];
|
||||||
@@ -1178,6 +1193,8 @@ async function processMessage(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const rawBody = text.trim() || placeholder;
|
const rawBody = text.trim() || placeholder;
|
||||||
|
const replyContext = formatReplyContext(message);
|
||||||
|
const baseBody = replyContext ? `${rawBody}\n\n${replyContext}` : rawBody;
|
||||||
const fromLabel = isGroup
|
const fromLabel = isGroup
|
||||||
? `group:${peerId}`
|
? `group:${peerId}`
|
||||||
: message.senderName || `user:${message.senderId}`;
|
: message.senderName || `user:${message.senderId}`;
|
||||||
@@ -1202,7 +1219,7 @@ async function processMessage(
|
|||||||
timestamp: message.timestamp,
|
timestamp: message.timestamp,
|
||||||
previousTimestamp,
|
previousTimestamp,
|
||||||
envelope: envelopeOptions,
|
envelope: envelopeOptions,
|
||||||
body: rawBody,
|
body: baseBody,
|
||||||
});
|
});
|
||||||
let chatGuidForActions = chatGuid;
|
let chatGuidForActions = chatGuid;
|
||||||
if (!chatGuidForActions && baseUrl && password) {
|
if (!chatGuidForActions && baseUrl && password) {
|
||||||
|
|||||||
@@ -589,6 +589,38 @@ describe("send", () => {
|
|||||||
expect(result.messageId).toBe("numeric-id-456");
|
expect(result.messageId).toBe("numeric-id-456");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("extracts messageGuid from response payload", async () => {
|
||||||
|
mockFetch
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
ok: true,
|
||||||
|
json: () =>
|
||||||
|
Promise.resolve({
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
guid: "iMessage;-;+15551234567",
|
||||||
|
participants: [{ address: "+15551234567" }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
ok: true,
|
||||||
|
text: () =>
|
||||||
|
Promise.resolve(
|
||||||
|
JSON.stringify({
|
||||||
|
data: { messageGuid: "msg-guid-789" },
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await sendMessageBlueBubbles("+15551234567", "Hello", {
|
||||||
|
serverUrl: "http://localhost:1234",
|
||||||
|
password: "test",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.messageId).toBe("msg-guid-789");
|
||||||
|
});
|
||||||
|
|
||||||
it("resolves credentials from config", async () => {
|
it("resolves credentials from config", async () => {
|
||||||
mockFetch
|
mockFetch
|
||||||
.mockResolvedValueOnce({
|
.mockResolvedValueOnce({
|
||||||
|
|||||||
@@ -86,12 +86,18 @@ function resolveSendTarget(raw: string): BlueBubblesSendTarget {
|
|||||||
function extractMessageId(payload: unknown): string {
|
function extractMessageId(payload: unknown): string {
|
||||||
if (!payload || typeof payload !== "object") return "unknown";
|
if (!payload || typeof payload !== "object") return "unknown";
|
||||||
const record = payload as Record<string, unknown>;
|
const record = payload as Record<string, unknown>;
|
||||||
const data = record.data && typeof record.data === "object" ? (record.data as Record<string, unknown>) : null;
|
const data =
|
||||||
|
record.data && typeof record.data === "object" ? (record.data as Record<string, unknown>) : null;
|
||||||
const candidates = [
|
const candidates = [
|
||||||
record.messageId,
|
record.messageId,
|
||||||
|
record.messageGuid,
|
||||||
|
record.message_guid,
|
||||||
record.guid,
|
record.guid,
|
||||||
record.id,
|
record.id,
|
||||||
data?.messageId,
|
data?.messageId,
|
||||||
|
data?.messageGuid,
|
||||||
|
data?.message_guid,
|
||||||
|
data?.message_id,
|
||||||
data?.guid,
|
data?.guid,
|
||||||
data?.id,
|
data?.id,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -156,9 +156,10 @@ export function buildEmbeddedRunPayloads(params: {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (replyItems.length === 0 && params.lastToolError) {
|
if (params.lastToolError) {
|
||||||
// Check if this is a recoverable/internal tool error that shouldn't be shown to users.
|
const hasUserFacingReply = replyItems.length > 0;
|
||||||
// These include parameter validation errors that the model should have retried.
|
// Check if this is a recoverable/internal tool error that shouldn't be shown to users
|
||||||
|
// when there's already a user-facing reply (the model should have retried).
|
||||||
const errorLower = (params.lastToolError.error ?? "").toLowerCase();
|
const errorLower = (params.lastToolError.error ?? "").toLowerCase();
|
||||||
const isRecoverableError =
|
const isRecoverableError =
|
||||||
errorLower.includes("required") ||
|
errorLower.includes("required") ||
|
||||||
@@ -169,8 +170,7 @@ export function buildEmbeddedRunPayloads(params: {
|
|||||||
errorLower.includes("needs") ||
|
errorLower.includes("needs") ||
|
||||||
errorLower.includes("requires");
|
errorLower.includes("requires");
|
||||||
|
|
||||||
// Only show non-recoverable errors to users
|
if (!hasUserFacingReply || !isRecoverableError) {
|
||||||
if (!isRecoverableError) {
|
|
||||||
const toolSummary = formatToolAggregate(
|
const toolSummary = formatToolAggregate(
|
||||||
params.lastToolError.toolName,
|
params.lastToolError.toolName,
|
||||||
params.lastToolError.meta ? [params.lastToolError.meta] : undefined,
|
params.lastToolError.meta ? [params.lastToolError.meta] : undefined,
|
||||||
@@ -182,8 +182,8 @@ export function buildEmbeddedRunPayloads(params: {
|
|||||||
isError: true,
|
isError: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// Note: Recoverable errors are already in the model's context as tool_result is_error,
|
// Note: Recoverable errors are already in the model's context as tool_result is_error.
|
||||||
// so the model can see them and should retry. We just don't send them to the user.
|
// We only suppress them when a user-facing reply already exists.
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasAudioAsVoiceTag = replyItems.some((item) => item.audioAsVoice);
|
const hasAudioAsVoiceTag = replyItems.some((item) => item.audioAsVoice);
|
||||||
|
|||||||
Reference in New Issue
Block a user