mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-28 09:33:06 +00:00
fix(feishu): use message create_time for inbound timestamps (#52809)
* fix(feishu): use message create_time instead of Date.now() for Timestamp field When a message is sent offline and later retried by the Feishu client upon reconnection, Date.now() captures the *delivery* time rather than the *authoring* time. This causes downstream consumers to see a timestamp that can be minutes or hours after the user actually composed the message, leading to incorrect temporal semantics — for example, a "delete this" command may target the wrong resource because the agent believes the instruction was issued much later than it actually was. Replace every Date.now() used for message timestamps with the original create_time from the Feishu event payload (millisecond-epoch string), falling back to Date.now() only when the field is absent. The definition is also hoisted to the top of handleFeishuMessage so that both the pending-history path and the main inbound-payload path share the same authoritative value. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test(feishu): verify Timestamp uses message create_time Add two test cases: 1. When create_time is present, Timestamp must equal the parsed value 2. When create_time is absent, Timestamp falls back to Date.now() Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: revert unrelated formatting change to lifecycle.test.ts This file was inadvertently formatted in a prior commit. Reverting to match main and keep the PR scoped to the Feishu timestamp fix only. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(feishu): use message create_time for inbound timestamps (#52809) (thanks @schumilin) --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: George Zhang <georgezhangtj97@gmail.com>
This commit is contained in:
@@ -733,6 +733,77 @@ describe("handleFeishuMessage command authorization", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("uses message create_time as Timestamp instead of Date.now()", async () => {
|
||||
mockShouldComputeCommandAuthorized.mockReturnValue(false);
|
||||
|
||||
const cfg: ClawdbotConfig = {
|
||||
channels: {
|
||||
feishu: {
|
||||
dmPolicy: "open",
|
||||
},
|
||||
},
|
||||
} as ClawdbotConfig;
|
||||
|
||||
const event: FeishuMessageEvent = {
|
||||
sender: {
|
||||
sender_id: {
|
||||
open_id: "ou-attacker",
|
||||
},
|
||||
},
|
||||
message: {
|
||||
message_id: "msg-create-time",
|
||||
chat_id: "oc-dm",
|
||||
chat_type: "p2p",
|
||||
message_type: "text",
|
||||
content: JSON.stringify({ text: "delete this" }),
|
||||
create_time: "1700000000000",
|
||||
},
|
||||
};
|
||||
|
||||
await dispatchMessage({ cfg, event });
|
||||
|
||||
expect(mockFinalizeInboundContext).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
Timestamp: 1700000000000,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to Date.now() when create_time is absent", async () => {
|
||||
mockShouldComputeCommandAuthorized.mockReturnValue(false);
|
||||
|
||||
const cfg: ClawdbotConfig = {
|
||||
channels: {
|
||||
feishu: {
|
||||
dmPolicy: "open",
|
||||
},
|
||||
},
|
||||
} as ClawdbotConfig;
|
||||
|
||||
const event: FeishuMessageEvent = {
|
||||
sender: {
|
||||
sender_id: {
|
||||
open_id: "ou-attacker",
|
||||
},
|
||||
},
|
||||
message: {
|
||||
message_id: "msg-no-create-time",
|
||||
chat_id: "oc-dm",
|
||||
chat_type: "p2p",
|
||||
message_type: "text",
|
||||
content: JSON.stringify({ text: "hello" }),
|
||||
},
|
||||
};
|
||||
|
||||
const before = Date.now();
|
||||
await dispatchMessage({ cfg, event });
|
||||
const after = Date.now();
|
||||
|
||||
const call = mockFinalizeInboundContext.mock.calls[0]?.[0] as { Timestamp: number };
|
||||
expect(call.Timestamp).toBeGreaterThanOrEqual(before);
|
||||
expect(call.Timestamp).toBeLessThanOrEqual(after);
|
||||
});
|
||||
|
||||
it("replies pairing challenge to DM chat_id instead of user:sender id", async () => {
|
||||
const cfg: ClawdbotConfig = {
|
||||
channels: {
|
||||
|
||||
@@ -357,6 +357,14 @@ export async function handleFeishuMessage(params: {
|
||||
? [...new Set(rawBroadcastAgents.map((id) => normalizeAgentId(id)))]
|
||||
: null;
|
||||
|
||||
// Parse message create_time early so every downstream consumer (pending
|
||||
// history, inbound payload, etc.) uses the original authoring timestamp
|
||||
// instead of the delivery/processing time. Feishu uses a millisecond
|
||||
// epoch string; fall back to Date.now() only when the field is absent.
|
||||
const messageCreateTimeMs = event.message.create_time
|
||||
? parseInt(event.message.create_time, 10)
|
||||
: Date.now();
|
||||
|
||||
let requireMention = false; // DMs never require mention; groups may override below
|
||||
if (isGroup) {
|
||||
if (groupConfig?.enabled === false) {
|
||||
@@ -434,7 +442,7 @@ export async function handleFeishuMessage(params: {
|
||||
entry: {
|
||||
sender: ctx.senderOpenId,
|
||||
body: `${ctx.senderName ?? ctx.senderOpenId}: ${ctx.content}`,
|
||||
timestamp: Date.now(),
|
||||
timestamp: messageCreateTimeMs,
|
||||
messageId: ctx.messageId,
|
||||
},
|
||||
});
|
||||
@@ -919,7 +927,7 @@ export async function handleFeishuMessage(params: {
|
||||
// Only use rootId (om_* message anchor) — threadId (omt_*) is a container
|
||||
// ID and would produce invalid reply targets downstream.
|
||||
MessageThreadId: ctx.rootId && isTopicSessionForThread ? ctx.rootId : undefined,
|
||||
Timestamp: Date.now(),
|
||||
Timestamp: messageCreateTimeMs,
|
||||
WasMentioned: wasMentioned,
|
||||
CommandAuthorized: commandAuthorized,
|
||||
OriginatingChannel: "feishu" as const,
|
||||
@@ -929,10 +937,6 @@ export async function handleFeishuMessage(params: {
|
||||
});
|
||||
};
|
||||
|
||||
// Parse message create_time (Feishu uses millisecond epoch string).
|
||||
const messageCreateTimeMs = event.message.create_time
|
||||
? parseInt(event.message.create_time, 10)
|
||||
: undefined;
|
||||
// Determine reply target based on group session mode:
|
||||
// - Topic-mode groups (group_topic / group_topic_sender): reply to the topic
|
||||
// root so the bot stays in the same thread.
|
||||
|
||||
Reference in New Issue
Block a user