mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:00:43 +00:00
fix: recognize attachment message sends
This commit is contained in:
@@ -86,6 +86,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Agents/generated media: treat attachment-style message tool actions as completed chat sends, preventing duplicate fallback media posts when generated files were already uploaded.
|
||||
- Discord/streaming: show live reasoning text in progress drafts instead of a bare `Reasoning` status line.
|
||||
- Doctor/status: warn when `OPENCLAW_GATEWAY_TOKEN` would shadow a different active `gateway.auth.token` source for local CLI commands, while avoiding false positives when config points at the same env token. Fixes #74271. Thanks @yelog.
|
||||
- Gateway/HTTP: avoid loading managed outgoing-image media handlers for unrelated requests, so disabled OpenAI-compatible routes return 404 without waiting on lazy media sidecars. Thanks @vincentkoc.
|
||||
|
||||
@@ -2,6 +2,18 @@ import { getChannelPlugin, normalizeChannelId } from "../channels/plugins/index.
|
||||
import { normalizeOptionalString } from "../shared/string-coerce.js";
|
||||
|
||||
const CORE_MESSAGING_TOOLS = new Set(["sessions_send", "message"]);
|
||||
const MESSAGE_TOOL_SEND_ACTIONS = new Set([
|
||||
"send",
|
||||
"thread-reply",
|
||||
"sendWithEffect",
|
||||
"sendAttachment",
|
||||
"upload-file",
|
||||
]);
|
||||
|
||||
export function isMessageToolSendActionName(action: unknown): boolean {
|
||||
const normalized = normalizeOptionalString(action) ?? "";
|
||||
return MESSAGE_TOOL_SEND_ACTIONS.has(normalized);
|
||||
}
|
||||
|
||||
// Provider docking: any plugin with `actions` opts into messaging tool handling.
|
||||
export function isMessagingTool(toolName: string): boolean {
|
||||
@@ -21,7 +33,7 @@ export function isMessagingToolSendAction(
|
||||
return true;
|
||||
}
|
||||
if (toolName === "message") {
|
||||
return action === "send" || action === "thread-reply";
|
||||
return isMessageToolSendActionName(action);
|
||||
}
|
||||
const providerId = normalizeChannelId(toolName);
|
||||
if (!providerId) {
|
||||
|
||||
@@ -941,6 +941,85 @@ describe("messaging tool media URL tracking", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("commits upload-file args as message delivery evidence", async () => {
|
||||
const { ctx } = createTestContext();
|
||||
|
||||
const startEvt: ToolExecutionStartEvent = {
|
||||
type: "tool_execution_start",
|
||||
toolName: "message",
|
||||
toolCallId: "tool-upload-file",
|
||||
args: {
|
||||
action: "upload-file",
|
||||
channel: "discord",
|
||||
to: "channel:123",
|
||||
message: "track ready",
|
||||
path: "/tmp/generated-song.mp3",
|
||||
},
|
||||
};
|
||||
await handleToolExecutionStart(ctx, startEvt);
|
||||
|
||||
expect(ctx.state.pendingMessagingMediaUrls.get("tool-upload-file")).toEqual([
|
||||
"/tmp/generated-song.mp3",
|
||||
]);
|
||||
|
||||
const endEvt: ToolExecutionEndEvent = {
|
||||
type: "tool_execution_end",
|
||||
toolName: "message",
|
||||
toolCallId: "tool-upload-file",
|
||||
isError: false,
|
||||
result: { ok: true },
|
||||
};
|
||||
await handleToolExecutionEnd(ctx, endEvt);
|
||||
|
||||
expect(ctx.state.messagingToolSentMediaUrls).toEqual(["/tmp/generated-song.mp3"]);
|
||||
expect(ctx.state.messagingToolSentTargets).toEqual([
|
||||
expect.objectContaining({
|
||||
provider: "discord",
|
||||
to: "channel:123",
|
||||
text: "track ready",
|
||||
mediaUrls: ["/tmp/generated-song.mp3"],
|
||||
}),
|
||||
]);
|
||||
expect(ctx.state.pendingMessagingMediaUrls.has("tool-upload-file")).toBe(false);
|
||||
});
|
||||
|
||||
it("commits sendAttachment args as message delivery evidence", async () => {
|
||||
const { ctx } = createTestContext();
|
||||
|
||||
const startEvt: ToolExecutionStartEvent = {
|
||||
type: "tool_execution_start",
|
||||
toolName: "message",
|
||||
toolCallId: "tool-send-attachment",
|
||||
args: {
|
||||
action: "sendAttachment",
|
||||
provider: "discord",
|
||||
to: "channel:123",
|
||||
content: "track ready",
|
||||
filePath: "/tmp/generated-song.mp3",
|
||||
},
|
||||
};
|
||||
await handleToolExecutionStart(ctx, startEvt);
|
||||
|
||||
const endEvt: ToolExecutionEndEvent = {
|
||||
type: "tool_execution_end",
|
||||
toolName: "message",
|
||||
toolCallId: "tool-send-attachment",
|
||||
isError: false,
|
||||
result: { ok: true },
|
||||
};
|
||||
await handleToolExecutionEnd(ctx, endEvt);
|
||||
|
||||
expect(ctx.state.messagingToolSentMediaUrls).toEqual(["/tmp/generated-song.mp3"]);
|
||||
expect(ctx.state.messagingToolSentTargets).toEqual([
|
||||
expect.objectContaining({
|
||||
provider: "discord",
|
||||
to: "channel:123",
|
||||
text: "track ready",
|
||||
mediaUrls: ["/tmp/generated-song.mp3"],
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("trims messagingToolSentMediaUrls to 200 on commit (FIFO)", async () => {
|
||||
const { ctx } = createTestContext();
|
||||
|
||||
|
||||
@@ -60,4 +60,58 @@ describe("extractMessagingToolSend", () => {
|
||||
expect(result?.provider).toBe("telegram");
|
||||
expect(result?.to).toBe("telegram:123");
|
||||
});
|
||||
|
||||
it("recognizes attachment-style message tool sends", () => {
|
||||
const upload = extractMessagingToolSend("message", {
|
||||
action: "upload-file",
|
||||
channel: "discord",
|
||||
to: "channel:123",
|
||||
path: "/tmp/song.mp3",
|
||||
});
|
||||
const attachment = extractMessagingToolSend("message", {
|
||||
action: "sendAttachment",
|
||||
provider: "discord",
|
||||
to: "channel:123",
|
||||
filePath: "/tmp/song.mp3",
|
||||
});
|
||||
const effect = extractMessagingToolSend("message", {
|
||||
action: "sendWithEffect",
|
||||
provider: "discord",
|
||||
to: "channel:123",
|
||||
content: "done",
|
||||
});
|
||||
|
||||
expect(upload).toMatchObject({
|
||||
tool: "message",
|
||||
provider: "discord",
|
||||
to: "channel:123",
|
||||
});
|
||||
expect(attachment).toMatchObject({
|
||||
tool: "message",
|
||||
provider: "discord",
|
||||
to: "channel:123",
|
||||
});
|
||||
expect(effect).toMatchObject({
|
||||
tool: "message",
|
||||
provider: "discord",
|
||||
to: "channel:123",
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps thread id evidence for thread replies", () => {
|
||||
const result = extractMessagingToolSend("message", {
|
||||
action: "thread-reply",
|
||||
provider: "discord",
|
||||
to: "channel:123",
|
||||
threadId: "456",
|
||||
content: "done",
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({
|
||||
tool: "message",
|
||||
provider: "discord",
|
||||
to: "channel:123",
|
||||
threadId: "456",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from "../shared/string-coerce.js";
|
||||
import { truncateUtf16Safe } from "../utils.js";
|
||||
import { collectTextContentBlocks } from "./content-blocks.js";
|
||||
import { isMessageToolSendActionName } from "./pi-embedded-messaging.js";
|
||||
import type { MessagingToolSend } from "./pi-embedded-messaging.types.js";
|
||||
import { normalizeToolName } from "./tool-policy.js";
|
||||
|
||||
@@ -539,7 +540,7 @@ export function extractMessagingToolSend(
|
||||
const action = normalizeOptionalString(args.action) ?? "";
|
||||
const accountId = normalizeOptionalString(args.accountId);
|
||||
if (toolName === "message") {
|
||||
if (action !== "send" && action !== "thread-reply") {
|
||||
if (!isMessageToolSendActionName(action)) {
|
||||
return undefined;
|
||||
}
|
||||
const toRaw = resolveMessageToolTarget(args);
|
||||
@@ -552,7 +553,8 @@ export function extractMessagingToolSend(
|
||||
const providerId = providerHint ? normalizeChannelId(providerHint) : null;
|
||||
const provider = providerId ?? normalizeOptionalLowercaseString(providerHint) ?? "message";
|
||||
const to = normalizeTargetForProvider(provider, toRaw);
|
||||
return to ? { tool: toolName, provider, accountId, to } : undefined;
|
||||
const threadId = normalizeOptionalString(args.threadId);
|
||||
return to ? { tool: toolName, provider, accountId, to, threadId } : undefined;
|
||||
}
|
||||
const providerId = normalizeChannelId(toolName);
|
||||
if (!providerId) {
|
||||
|
||||
Reference in New Issue
Block a user