mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
fix(feishu): recover recent files from disk when in-memory cache misses
The per-chat recent file cache (Caffeine, 60 min TTL) is purely
in-memory. After a process restart, GC eviction, or TTL expiry the
cache is empty, but the staged copies under data/chat-uploads/ survive
on disk. A follow-up text message that should have seen the cached file
instead found nothing — the bot replied as if no file was ever sent.
Changes:
- injectRecentFiles(): fall back to scanning data/chat-uploads/{id}/
when the Caffeine cache misses, sorted by last-modified time, capped
at RECENT_FILE_MAX_PER_CHAT (5).
- cacheRecentFile(): promote catch log from debug → warn with full
stack trace so silent download failures are visible in production
logs. Add entry-level info log for correlation.
- New helper loadRecentFilesFromDisk() + guessContentType().
Closes #325
Relates to #201
This commit is contained in:
parent
32ad11d6c4
commit
39a55db65f
@ -1669,6 +1669,7 @@ public class FeishuChannelAdapter extends AbstractChannelAdapter implements Stre
|
||||
private String cacheRecentFile(String messageId, String messageType, String contentStr,
|
||||
String conversationId) {
|
||||
try {
|
||||
log.info("[feishu] cacheRecentFile: type={}, conversationId={}, messageId={}", messageType, conversationId, messageId);
|
||||
Map<String, Object> contentObj = objectMapper.readValue(contentStr, Map.class);
|
||||
|
||||
String fileKey = null;
|
||||
@ -1738,7 +1739,7 @@ public class FeishuChannelAdapter extends AbstractChannelAdapter implements Stre
|
||||
|
||||
return dest.toAbsolutePath().toString();
|
||||
} catch (Exception e) {
|
||||
log.debug("[feishu] Failed to cache recent file: {}", e.getMessage());
|
||||
log.warn("[feishu] Failed to cache recent file for conversation={}: {}", conversationId, e.getMessage(), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -1752,7 +1753,14 @@ public class FeishuChannelAdapter extends AbstractChannelAdapter implements Stre
|
||||
*/
|
||||
private String injectRecentFiles(String conversationId, List<MessageContentPart> parts, String textContent) {
|
||||
List<RecentFileEntry> recent = recentFileCache.getIfPresent(conversationId);
|
||||
if (recent == null || recent.isEmpty()) return textContent;
|
||||
if (recent == null || recent.isEmpty()) {
|
||||
// Fallback: scan data/chat-uploads/{conversationId}/ on disk.
|
||||
// Survives process restart / Caffeine TTL expiry / GC eviction.
|
||||
recent = loadRecentFilesFromDisk(conversationId);
|
||||
if (recent.isEmpty()) return textContent;
|
||||
log.info("[feishu] injectRecentFiles: cache miss, recovered {} file(s) from disk for conversation={}",
|
||||
recent.size(), conversationId);
|
||||
}
|
||||
|
||||
// Collect paths already in parts to avoid duplicates
|
||||
Set<String> existingPaths = new java.util.HashSet<>();
|
||||
@ -1782,6 +1790,75 @@ public class FeishuChannelAdapter extends AbstractChannelAdapter implements Stre
|
||||
return text.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan {@code data/chat-uploads/{conversationId}/} on disk and return
|
||||
* the most recent files as {@link RecentFileEntry}s. Used as a
|
||||
* fallback when the in-memory Caffeine cache has been evicted
|
||||
* (process restart, TTL expiry, GC pressure) but the staged copies
|
||||
* are still on disk.
|
||||
*/
|
||||
private List<RecentFileEntry> loadRecentFilesFromDisk(String conversationId) {
|
||||
Path dir = Path.of("data", "chat-uploads", conversationId);
|
||||
if (!Files.isDirectory(dir)) return List.of();
|
||||
try (var stream = Files.list(dir)) {
|
||||
return stream
|
||||
.filter(Files::isRegularFile)
|
||||
.sorted((a, b) -> {
|
||||
try {
|
||||
return Long.compare(
|
||||
Files.getLastModifiedTime(b).toMillis(),
|
||||
Files.getLastModifiedTime(a).toMillis());
|
||||
} catch (Exception e) {
|
||||
return 0;
|
||||
}
|
||||
})
|
||||
.limit(RECENT_FILE_MAX_PER_CHAT)
|
||||
.map(p -> {
|
||||
String fileName = p.getFileName().toString();
|
||||
// Strip timestamp prefix (e.g. "1777391026594_report.pdf" → "report.pdf")
|
||||
int sep = fileName.indexOf('_');
|
||||
String display = (sep > 0 && sep < 20) ? fileName.substring(sep + 1) : fileName;
|
||||
String contentType = guessContentType(p);
|
||||
return new RecentFileEntry(display, p.toAbsolutePath().toString(), null, contentType);
|
||||
})
|
||||
.toList();
|
||||
} catch (Exception e) {
|
||||
log.debug("[feishu] Failed to scan disk for recent files in {}: {}", dir, e.getMessage());
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
/** Best-effort content type from file extension. */
|
||||
private static String guessContentType(Path p) {
|
||||
String name = p.getFileName().toString().toLowerCase();
|
||||
if (name.endsWith(".pdf")) return "application/pdf";
|
||||
if (name.endsWith(".docx")) return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
|
||||
if (name.endsWith(".xlsx")) return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
|
||||
if (name.endsWith(".pptx")) return "application/vnd.openxmlformats-officedocument.presentationml.presentation";
|
||||
if (name.endsWith(".doc")) return "application/msword";
|
||||
if (name.endsWith(".xls")) return "application/vnd.ms-excel";
|
||||
if (name.endsWith(".ppt")) return "application/vnd.ms-powerpoint";
|
||||
if (name.endsWith(".txt")) return "text/plain";
|
||||
if (name.endsWith(".csv")) return "text/csv";
|
||||
if (name.endsWith(".json")) return "application/json";
|
||||
if (name.endsWith(".xml")) return "application/xml";
|
||||
if (name.endsWith(".md")) return "text/markdown";
|
||||
if (name.endsWith(".png")) return "image/png";
|
||||
if (name.endsWith(".jpg") || name.endsWith(".jpeg")) return "image/jpeg";
|
||||
if (name.endsWith(".gif")) return "image/gif";
|
||||
if (name.endsWith(".webp")) return "image/webp";
|
||||
if (name.endsWith(".mp3")) return "audio/mpeg";
|
||||
if (name.endsWith(".ogg")) return "audio/ogg";
|
||||
if (name.endsWith(".opus")) return "audio/opus";
|
||||
if (name.endsWith(".wav")) return "audio/wav";
|
||||
if (name.endsWith(".mp4")) return "video/mp4";
|
||||
if (name.endsWith(".mov")) return "video/quicktime";
|
||||
if (name.endsWith(".zip")) return "application/zip";
|
||||
if (name.endsWith(".rar")) return "application/x-rar-compressed";
|
||||
if (name.endsWith(".7z")) return "application/x-7z-compressed";
|
||||
return "application/octet-stream";
|
||||
}
|
||||
|
||||
// ==================== 消息内容解析 ====================
|
||||
|
||||
/**
|
||||
|
||||
Loading…
Reference in New Issue
Block a user