From b02f2ebfeec7ffd48a8ae457bbc561ef8f12d44e Mon Sep 17 00:00:00 2001 From: matevip Date: Tue, 21 Apr 2026 17:34:28 +0800 Subject: [PATCH] fix(memory): HiL edit binds key to report's candidate entries --- .../memory/controller/DreamController.java | 31 +++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/mateclaw-server/src/main/java/vip/mate/memory/controller/DreamController.java b/mateclaw-server/src/main/java/vip/mate/memory/controller/DreamController.java index 285c9067..195a825d 100644 --- a/mateclaw-server/src/main/java/vip/mate/memory/controller/DreamController.java +++ b/mateclaw-server/src/main/java/vip/mate/memory/controller/DreamController.java @@ -11,7 +11,9 @@ import vip.mate.common.result.R; import org.springframework.http.MediaType; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import vip.mate.memory.model.DreamReportEntity; +import vip.mate.memory.model.MemoryRecallEntity; import vip.mate.memory.repository.DreamReportMapper; +import vip.mate.memory.repository.MemoryRecallMapper; import vip.mate.memory.service.MorningCardService; import vip.mate.memory.service.MemoryHilService; import vip.mate.workspace.core.annotation.RequireWorkspaceRole; @@ -32,6 +34,7 @@ import java.util.Map; public class DreamController { private final DreamReportMapper dreamReportMapper; + private final MemoryRecallMapper recallMapper; private final MorningCardService morningCardService; private final MemoryHilService hilService; private final DreamEventBroadcaster eventBroadcaster; @@ -129,8 +132,10 @@ public class DreamController { @PathVariable Long reportId, @PathVariable String key, @RequestBody Map body) { - // Validate reportId belongs to this agent (0 = direct edit from MemoryBrowser) + String decodedKey = java.net.URLDecoder.decode(key, java.nio.charset.StandardCharsets.UTF_8); + if (reportId != 0L) { + // Report-scoped edit: validate report belongs to agent AND key belongs to report DreamReportEntity report = dreamReportMapper.selectOne( new LambdaQueryWrapper() .eq(DreamReportEntity::getId, reportId) @@ -139,12 +144,26 @@ public class DreamController { if (report == null) { return R.fail("Report not found or does not belong to this agent"); } + // Key must match a recall entry that was a candidate during this dream run + // (lastRecalledAt between report.startedAt and report.finishedAt) + List candidates = recallMapper.selectList( + new LambdaQueryWrapper() + .eq(MemoryRecallEntity::getAgentId, agentId) + .ge(MemoryRecallEntity::getLastRecalledAt, report.getStartedAt()) + .le(MemoryRecallEntity::getLastRecalledAt, report.getFinishedAt()) + .eq(MemoryRecallEntity::getDeleted, 0)); + boolean keyBelongsToReport = candidates.stream() + .anyMatch(c -> c.getFilename() != null && c.getFilename().contains(decodedKey)); + if (!keyBelongsToReport) { + return R.fail("Entry '" + decodedKey + "' does not belong to report " + reportId); + } + } else { + // Direct edit (reportId=0, from MemoryBrowser): only require section exists + if (!hilService.sectionExists(agentId, decodedKey)) { + return R.fail("Section '" + decodedKey + "' not found in MEMORY.md"); + } } - // Validate key is an existing section in MEMORY.md (prevent arbitrary edits) - String decodedKey = java.net.URLDecoder.decode(key, java.nio.charset.StandardCharsets.UTF_8); - if (!hilService.sectionExists(agentId, decodedKey)) { - return R.fail("Section '" + decodedKey + "' not found in MEMORY.md"); - } + String newContent = body.get("content"); if (newContent == null || newContent.isBlank()) { return R.fail("content is required");