fix(feishu): prefer video file_key for inbound media

This commit is contained in:
Peter Steinberger
2026-02-22 19:21:01 +01:00
parent 0efe2cab7d
commit 1bc5ba6e29
3 changed files with 82 additions and 11 deletions

View File

@@ -35,6 +35,7 @@ Docs: https://docs.openclaw.ai
- Dev tooling: prevent `CLAUDE.md` symlink target regressions by excluding CLAUDE symlink sentinels from `oxfmt` and marking them `-text` in `.gitattributes`, so formatter/EOL normalization cannot reintroduce trailing-newline targets. Thanks @vincentkoc.
- Cron: honor `cron.maxConcurrentRuns` in the timer loop so due jobs can execute up to the configured parallelism instead of always running serially. (#11595) Thanks @Takhoffman.
- Agents/Compaction: restore embedded compaction safeguard/context-pruning extension loading in production by wiring bundled extension factories into the resource loader instead of runtime file-path resolution. (#22349) Thanks @Glucksberg.
- Feishu/Media: for inbound video messages that include both `file_key` (video) and `image_key` (thumbnail), prefer `file_key` when downloading media so video attachments are saved instead of silently failing on thumbnail keys. (#23633)
- Hooks/Cron: suppress duplicate main-session events for delivered hook turns and mark `SILENT_REPLY_TOKEN` (`NO_REPLY`) early exits as delivered to prevent hook context pollution. (#20678) Thanks @JonathanWorks.
- Providers/OpenRouter: inject `cache_control` on system prompts for OpenRouter Anthropic models to improve prompt-cache reuse. (#17473) Thanks @rrenamed.
- Installer/Smoke tests: remove legacy `OPENCLAW_USE_GUM` overrides from docker install-smoke runs so tests exercise installer auto TTY detection behavior directly.

View File

@@ -4,17 +4,25 @@ import type { FeishuMessageEvent } from "./bot.js";
import { handleFeishuMessage } from "./bot.js";
import { setFeishuRuntime } from "./runtime.js";
const { mockCreateFeishuReplyDispatcher, mockSendMessageFeishu, mockGetMessageFeishu } = vi.hoisted(
() => ({
mockCreateFeishuReplyDispatcher: vi.fn(() => ({
dispatcher: vi.fn(),
replyOptions: {},
markDispatchIdle: vi.fn(),
})),
mockSendMessageFeishu: vi.fn().mockResolvedValue({ messageId: "pairing-msg", chatId: "oc-dm" }),
mockGetMessageFeishu: vi.fn().mockResolvedValue(null),
const {
mockCreateFeishuReplyDispatcher,
mockSendMessageFeishu,
mockGetMessageFeishu,
mockDownloadMessageResourceFeishu,
} = vi.hoisted(() => ({
mockCreateFeishuReplyDispatcher: vi.fn(() => ({
dispatcher: vi.fn(),
replyOptions: {},
markDispatchIdle: vi.fn(),
})),
mockSendMessageFeishu: vi.fn().mockResolvedValue({ messageId: "pairing-msg", chatId: "oc-dm" }),
mockGetMessageFeishu: vi.fn().mockResolvedValue(null),
mockDownloadMessageResourceFeishu: vi.fn().mockResolvedValue({
buffer: Buffer.from("video"),
contentType: "video/mp4",
fileName: "clip.mp4",
}),
);
}));
vi.mock("./reply-dispatcher.js", () => ({
createFeishuReplyDispatcher: mockCreateFeishuReplyDispatcher,
@@ -25,6 +33,10 @@ vi.mock("./send.js", () => ({
getMessageFeishu: mockGetMessageFeishu,
}));
vi.mock("./media.js", () => ({
downloadMessageResourceFeishu: mockDownloadMessageResourceFeishu,
}));
function createRuntimeEnv(): RuntimeEnv {
return {
log: vi.fn(),
@@ -53,6 +65,10 @@ describe("handleFeishuMessage command authorization", () => {
const mockReadAllowFromStore = vi.fn().mockResolvedValue([]);
const mockUpsertPairingRequest = vi.fn().mockResolvedValue({ code: "ABCDEFGH", created: false });
const mockBuildPairingReply = vi.fn(() => "Pairing response");
const mockSaveMediaBuffer = vi.fn().mockResolvedValue({
path: "/tmp/inbound-clip.mp4",
contentType: "video/mp4",
});
beforeEach(() => {
vi.clearAllMocks();
@@ -79,12 +95,18 @@ describe("handleFeishuMessage command authorization", () => {
shouldComputeCommandAuthorized: mockShouldComputeCommandAuthorized,
resolveCommandAuthorizedFromAuthorizers: mockResolveCommandAuthorizedFromAuthorizers,
},
media: {
saveMediaBuffer: mockSaveMediaBuffer,
},
pairing: {
readAllowFromStore: mockReadAllowFromStore,
upsertPairingRequest: mockUpsertPairingRequest,
buildPairingReply: mockBuildPairingReply,
},
},
media: {
detectMime: vi.fn(async () => "application/octet-stream"),
},
} as unknown as PluginRuntime);
});
@@ -312,4 +334,52 @@ describe("handleFeishuMessage command authorization", () => {
}),
);
});
it("uses video file_key (not thumbnail image_key) for inbound video download", async () => {
mockShouldComputeCommandAuthorized.mockReturnValue(false);
const cfg: ClawdbotConfig = {
channels: {
feishu: {
dmPolicy: "open",
},
},
} as ClawdbotConfig;
const event: FeishuMessageEvent = {
sender: {
sender_id: {
open_id: "ou-sender",
},
},
message: {
message_id: "msg-video-inbound",
chat_id: "oc-dm",
chat_type: "p2p",
message_type: "video",
content: JSON.stringify({
file_key: "file_video_payload",
image_key: "img_thumb_payload",
file_name: "clip.mp4",
}),
},
};
await dispatchMessage({ cfg, event });
expect(mockDownloadMessageResourceFeishu).toHaveBeenCalledWith(
expect.objectContaining({
messageId: "msg-video-inbound",
fileKey: "file_video_payload",
type: "file",
}),
);
expect(mockSaveMediaBuffer).toHaveBeenCalledWith(
expect.any(Buffer),
"video/mp4",
"inbound",
expect.any(Number),
"clip.mp4",
);
});
});

View File

@@ -412,7 +412,7 @@ async function resolveFeishuMediaList(params: {
// For message media, always use messageResource API
// The image.get API is only for images uploaded via im/v1/images, not for message attachments
const fileKey = mediaKeys.imageKey || mediaKeys.fileKey;
const fileKey = mediaKeys.fileKey || mediaKeys.imageKey;
if (!fileKey) {
return [];
}