fix(feishu): mirror router conversationId fallback for recent-file cache

This commit is contained in:
matevip 2026-05-29 10:15:06 +08:00
parent f8088088b5
commit 96ed3e8aa7
2 changed files with 81 additions and 2 deletions

View File

@ -1412,8 +1412,13 @@ public class FeishuChannelAdapter extends AbstractChannelAdapter implements Stre
* {@code senderId} is the full open id. Mirror that exactly:
* {@code groups feishu:{shortSuffix}}, {@code DMs feishu:{senderOpenId}}.
*/
private String buildConversationId(String shortSuffix, String senderOpenId, boolean isGroup) {
String identifier = isGroup ? shortSuffix : senderOpenId;
static String buildConversationId(String shortSuffix, String senderOpenId, boolean isGroup) {
// The routed ChannelMessage carries chatId = (isGroup ? shortSuffix : null);
// the router then falls back to senderId when that chatId is null. Mirror both
// steps so the storage id matches the runtime id in every case (including the
// degenerate group-with-no-suffix path).
String routedChatId = isGroup ? shortSuffix : null;
String identifier = routedChatId != null ? routedChatId : senderOpenId;
return identifier != null ? CHANNEL_TYPE + ":" + identifier : null;
}

View File

@ -0,0 +1,74 @@
package vip.mate.channel.feishu;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
/**
* Locks the invariant that {@link FeishuChannelAdapter#buildConversationId} produces
* exactly the id {@code ChannelMessageRouter} derives for the same chat.
*
* <p>The recent-file cache saves inbound attachments under
* {@code data/chat-uploads/{conversationId}/}; the prompt only exposes a file's
* name (not its path) to the model, so {@code ReadFileTool}/{@code DocumentExtractTool}
* resolve it through {@code ChatUploadResolver} under the runtime conversationId.
* If the storage id and the runtime id diverge, those tools cannot find the file
* and document reads silently fail which is precisely the regression these tests
* guard against.
*
* <p>The router derives its id from the routed {@code ChannelMessage}, whose
* {@code chatId} is {@code (isGroup ? shortSuffix : null)} and whose {@code senderId}
* is the full open id, via {@code feishu:{chatId != null ? chatId : senderId}}.
*/
class FeishuConversationIdAlignmentTest {
private static final String CHANNEL = FeishuChannelAdapter.CHANNEL_TYPE; // "feishu"
private static final String SHORT_SUFFIX = "cli2_abcd1234";
private static final String SENDER = "ou_user0123456789";
/** Mirror of ChannelMessageRouter#buildConversationId against the routed message. */
private static String routerConversationId(String shortSuffix, String senderId, boolean isGroup) {
String routedChatId = isGroup ? shortSuffix : null;
String identifier = routedChatId != null ? routedChatId : senderId;
return identifier != null ? CHANNEL + ":" + identifier : null;
}
@Test
void group_usesShortSuffix() {
assertEquals(CHANNEL + ":" + SHORT_SUFFIX,
FeishuChannelAdapter.buildConversationId(SHORT_SUFFIX, SENDER, true));
}
@Test
void dm_usesSenderOpenIdAndIgnoresShortSuffix() {
assertEquals(CHANNEL + ":" + SENDER,
FeishuChannelAdapter.buildConversationId(SHORT_SUFFIX, SENDER, false));
}
@Test
void group_nullShortSuffix_fallsBackToSender() {
// Degenerate group path: routed chatId is null, so the router (and this helper)
// fall back to the sender open id never null when a sender is present.
assertEquals(CHANNEL + ":" + SENDER,
FeishuChannelAdapter.buildConversationId(null, SENDER, true));
}
@Test
void dm_nullSender_returnsNull() {
assertNull(FeishuChannelAdapter.buildConversationId(SHORT_SUFFIX, null, false));
}
@Test
void matchesRouterFormula_acrossCases() {
// Group and DM, with and without a short suffix the storage id must equal
// the id the router computes for the routed ChannelMessage in every case.
assertEquals(routerConversationId(SHORT_SUFFIX, SENDER, true),
FeishuChannelAdapter.buildConversationId(SHORT_SUFFIX, SENDER, true));
assertEquals(routerConversationId(SHORT_SUFFIX, SENDER, false),
FeishuChannelAdapter.buildConversationId(SHORT_SUFFIX, SENDER, false));
assertEquals(routerConversationId(null, SENDER, true),
FeishuChannelAdapter.buildConversationId(null, SENDER, true));
assertEquals(routerConversationId(SHORT_SUFFIX, null, false),
FeishuChannelAdapter.buildConversationId(SHORT_SUFFIX, null, false));
}
}