mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-18 20:04:45 +00:00
fix(imessage): avoid visible media placeholder text (#81209)
Keep media-only iMessage sends from delivering visible <media:image> text while preserving a non-visible echo key for self-echo dedupe. Thanks @homer-byte.
This commit is contained in:
@@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- iMessage: stop sending visible `<media:image>` placeholder text for media-only native image sends while preserving the internal echo key that prevents self-echo duplicate replies. (#81209) Thanks @homer-byte.
|
||||
- Agents/sessions: create configured agent main sessions before first `sessions_send` or gateway send, so agent-to-agent messages no longer fail when the target agent has not started yet.
|
||||
- gateway: pass Talk session scope to resolver [AI]. (#81379) Thanks @pgondhi987.
|
||||
- Gateway protocol: require v4 clients and stream explicit chat `deltaText`/`replace` frames so SDK clients can consume assistant updates without local diffing. (#80725) Thanks @samzong.
|
||||
|
||||
@@ -237,11 +237,12 @@ describe("deliverReplies", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("records the actual sent placeholder for media-only replies", async () => {
|
||||
it("records the internal echo key for media-only replies", async () => {
|
||||
const remember = vi.fn();
|
||||
sendMessageIMessageMock.mockResolvedValueOnce({
|
||||
messageId: "imsg-media-1",
|
||||
sentText: "<media:image>",
|
||||
sentText: "",
|
||||
echoText: "<media:image>",
|
||||
});
|
||||
|
||||
await deliverReplies({
|
||||
|
||||
@@ -58,7 +58,10 @@ export async function deliverReplies(params: {
|
||||
// not before. The window between send completion and cache write is sub-millisecond;
|
||||
// the next SQLite inbound poll is 1-2s away, so no echo can arrive before the
|
||||
// cache entry exists.
|
||||
sentMessageCache?.remember(scope, { text: sent.sentText, messageId: sent.messageId });
|
||||
sentMessageCache?.remember(scope, {
|
||||
text: sent.echoText ?? sent.sentText,
|
||||
messageId: sent.messageId,
|
||||
});
|
||||
},
|
||||
sendMedia: async ({ mediaUrl, caption }) => {
|
||||
const sent = await sendMessageIMessage(target, caption ?? "", {
|
||||
@@ -70,7 +73,7 @@ export async function deliverReplies(params: {
|
||||
replyToId: payload.replyToId,
|
||||
});
|
||||
sentMessageCache?.remember(scope, {
|
||||
text: sent.sentText || undefined,
|
||||
text: sent.echoText ?? (sent.sentText || undefined),
|
||||
messageId: sent.messageId,
|
||||
});
|
||||
},
|
||||
@@ -94,7 +97,7 @@ export function createIMessageEchoCachingSend(params: {
|
||||
});
|
||||
const scope = `${params.accountId ?? opts.accountId ?? ""}:${target}`;
|
||||
params.sentMessageCache?.remember(scope, {
|
||||
text: sent.sentText || undefined,
|
||||
text: sent.echoText ?? (sent.sentText || undefined),
|
||||
messageId: sent.messageId,
|
||||
});
|
||||
return sent;
|
||||
|
||||
@@ -31,6 +31,7 @@ describe("sendMessageIMessage receipts", () => {
|
||||
|
||||
expect(result.messageId).toBe("p:0/imsg-1");
|
||||
expect(result.sentText).toBe("hello");
|
||||
expect(result.echoText).toBe("hello");
|
||||
expect(result.receipt.primaryPlatformMessageId).toBe("p:0/imsg-1");
|
||||
expect(result.receipt.platformMessageIds).toEqual(["p:0/imsg-1"]);
|
||||
expect(result.receipt.replyToId).toBe("reply-1");
|
||||
@@ -70,9 +71,19 @@ describe("sendMessageIMessage receipts", () => {
|
||||
});
|
||||
|
||||
expect(result.messageId).toBe("12345");
|
||||
expect(result.sentText).toBe("<media:image>");
|
||||
expect(result.sentText).toBe("");
|
||||
expect(result.echoText).toBe("<media:image>");
|
||||
expect(result.receipt.primaryPlatformMessageId).toBe("12345");
|
||||
expect(result.receipt.platformMessageIds).toEqual(["12345"]);
|
||||
expect(client.request).toHaveBeenCalledWith(
|
||||
"send",
|
||||
expect.objectContaining({
|
||||
chat_guid: "chat-1",
|
||||
file: "/tmp/image.png",
|
||||
text: "",
|
||||
}),
|
||||
expect.any(Object),
|
||||
);
|
||||
expect(result.receipt.raw).toEqual([
|
||||
{
|
||||
channel: "imessage",
|
||||
@@ -97,6 +108,26 @@ describe("sendMessageIMessage receipts", () => {
|
||||
expect(result.receipt.sentAt).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("preserves literal media placeholder text when no attachment is sent", async () => {
|
||||
const client = createClient({ guid: "p:0/imsg-text" });
|
||||
|
||||
const result = await sendMessageIMessage("chat_id:42", "literal <media:image> text", {
|
||||
config: IMESSAGE_TEST_CFG,
|
||||
client,
|
||||
});
|
||||
|
||||
expect(result.sentText).toBe("literal <media:image> text");
|
||||
expect(result.echoText).toBe("literal <media:image> text");
|
||||
expect(client.request).toHaveBeenCalledWith(
|
||||
"send",
|
||||
expect.objectContaining({
|
||||
chat_id: 42,
|
||||
text: "literal <media:image> text",
|
||||
}),
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not treat compatibility ok responses as visible platform ids", async () => {
|
||||
const client = createClient({ ok: "true" });
|
||||
|
||||
|
||||
@@ -6,8 +6,7 @@ import {
|
||||
} from "openclaw/plugin-sdk/channel-message";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/markdown-table-runtime";
|
||||
import { kindFromMime } from "openclaw/plugin-sdk/media-runtime";
|
||||
import { resolveOutboundAttachmentFromUrl } from "openclaw/plugin-sdk/media-runtime";
|
||||
import { kindFromMime, resolveOutboundAttachmentFromUrl } from "openclaw/plugin-sdk/media-runtime";
|
||||
import { requireRuntimeConfig } from "openclaw/plugin-sdk/plugin-config-runtime";
|
||||
import { convertMarkdownTables } from "openclaw/plugin-sdk/text-chunking";
|
||||
import { stripInlineDirectiveTagsForDelivery } from "openclaw/plugin-sdk/text-chunking";
|
||||
@@ -48,6 +47,7 @@ type IMessageSendOpts = {
|
||||
type IMessageSendResult = {
|
||||
messageId: string;
|
||||
sentText: string;
|
||||
echoText?: string;
|
||||
receipt: MessageReceipt;
|
||||
};
|
||||
|
||||
@@ -94,13 +94,13 @@ function resolveMessageId(result: Record<string, unknown> | null | undefined): s
|
||||
return raw ? raw.trim() : null;
|
||||
}
|
||||
|
||||
function resolveDeliveredIMessageText(text: string, mediaContentType?: string): string {
|
||||
function resolveOutboundEchoText(text: string, mediaContentType?: string): string | undefined {
|
||||
if (text.trim()) {
|
||||
return text;
|
||||
}
|
||||
const kind = kindFromMime(mediaContentType ?? undefined);
|
||||
if (!kind) {
|
||||
return text;
|
||||
return undefined;
|
||||
}
|
||||
return kind === "image" ? "<media:image>" : `<media:${kind}>`;
|
||||
}
|
||||
@@ -187,6 +187,7 @@ export async function sendMessageIMessage(
|
||||
: 16 * 1024 * 1024;
|
||||
let message = text ?? "";
|
||||
let filePath: string | undefined;
|
||||
let mediaContentType: string | undefined;
|
||||
|
||||
if (opts.mediaUrl?.trim()) {
|
||||
const resolveAttachmentFn = opts.resolveAttachmentImpl ?? resolveOutboundAttachmentFromUrl;
|
||||
@@ -195,7 +196,7 @@ export async function sendMessageIMessage(
|
||||
readFile: opts.mediaReadFile,
|
||||
});
|
||||
filePath = resolved.path;
|
||||
message = resolveDeliveredIMessageText(message, resolved.contentType ?? undefined);
|
||||
mediaContentType = resolved.contentType ?? undefined;
|
||||
}
|
||||
|
||||
if (!message.trim() && !filePath) {
|
||||
@@ -224,6 +225,7 @@ export async function sendMessageIMessage(
|
||||
if (!message.trim() && !filePath) {
|
||||
throw new Error("iMessage send requires text or media");
|
||||
}
|
||||
const echoText = resolveOutboundEchoText(message, filePath ? mediaContentType : undefined);
|
||||
const resolvedReplyToId = sanitizeReplyToId(opts.replyToId);
|
||||
const params: Record<string, unknown> = {
|
||||
text: message,
|
||||
@@ -266,7 +268,7 @@ export async function sendMessageIMessage(
|
||||
if (echoScope) {
|
||||
rememberPersistedIMessageEcho({
|
||||
scope: echoScope,
|
||||
text: message,
|
||||
text: echoText,
|
||||
messageId: resolvedId ?? undefined,
|
||||
});
|
||||
}
|
||||
@@ -293,6 +295,7 @@ export async function sendMessageIMessage(
|
||||
return {
|
||||
messageId,
|
||||
sentText: message,
|
||||
...(echoText ? { echoText } : {}),
|
||||
receipt: createIMessageSendReceipt({
|
||||
messageId,
|
||||
target,
|
||||
|
||||
Reference in New Issue
Block a user