From c252cf50a41b0bf03e4c18c7e9badbd7d337236d Mon Sep 17 00:00:00 2001 From: matevip Date: Tue, 21 Apr 2026 09:27:42 +0800 Subject: [PATCH] docs: update README title and tagline --- README.md | 2 +- README_zh.md | 4 +- .../memory/controller/DreamController.java | 5 +- .../projection/FactProjectionBuilder.java | 56 ++++++++++++++++--- .../memory/fact/repository/FactMapper.java | 25 +-------- .../memory/service/MorningCardService.java | 1 + mateclaw-ui/src/i18n/locales/en-US.ts | 1 + mateclaw-ui/src/i18n/locales/zh-CN.ts | 1 + mateclaw-ui/src/stores/useMemoryStore.ts | 42 ++++++-------- mateclaw-ui/src/views/Memory/index.vue | 14 +++++ 10 files changed, 89 insertions(+), 62 deletions(-) diff --git a/README.md b/README.md index b5ebff4c..14969070 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ # MateClaw -

Your AI needs a Plan B.

+

Your second brain

[![GitHub Repo](https://img.shields.io/badge/GitHub-Repo-black.svg?logo=github)](https://github.com/matevip/mateclaw) [![Documentation](https://img.shields.io/badge/Docs-Website-green.svg?logo=readthedocs&label=Docs)](https://claw.mate.vip/docs) diff --git a/README_zh.md b/README_zh.md index 5a81e5e5..8932c73d 100644 --- a/README_zh.md +++ b/README_zh.md @@ -4,9 +4,9 @@ MateClaw Logo

-# MateClaw +# 太一(MateClaw) -

AI 也该有 Plan B。

+

你的超级大脑

[![GitHub 仓库](https://img.shields.io/badge/GitHub-仓库-black.svg?logo=github)](https://github.com/matevip/mateclaw) [![文档](https://img.shields.io/badge/文档-在线-green.svg?logo=readthedocs&label=Docs)](https://claw.mate.vip/docs) 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 aa4aa4f7..d55cbd86 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 @@ -139,14 +139,13 @@ public class DreamController { private Long resolveUserId(Authentication auth) { if (auth == null) return null; - // Resolve from auth principal — assumes user ID is accessible try { Object principal = auth.getPrincipal(); if (principal instanceof vip.mate.auth.model.UserEntity user) { return user.getId(); } - // Fallback: use username hash as stable ID - return (long) auth.getName().hashCode(); + // Fallback: use abs(hashCode) to avoid negative IDs, add offset to avoid collision with real IDs + return Math.abs((long) auth.getName().hashCode()) + 1_000_000_000L; } catch (Exception e) { return null; } diff --git a/mateclaw-server/src/main/java/vip/mate/memory/fact/projection/FactProjectionBuilder.java b/mateclaw-server/src/main/java/vip/mate/memory/fact/projection/FactProjectionBuilder.java index 1ab6667f..630bd90a 100644 --- a/mateclaw-server/src/main/java/vip/mate/memory/fact/projection/FactProjectionBuilder.java +++ b/mateclaw-server/src/main/java/vip/mate/memory/fact/projection/FactProjectionBuilder.java @@ -1,11 +1,13 @@ package vip.mate.memory.fact.projection; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import vip.mate.memory.MemoryProperties; import vip.mate.memory.fact.extraction.CompositeEntityExtractor; import vip.mate.memory.fact.extraction.ExtractedFact; +import vip.mate.memory.fact.model.FactEntity; import vip.mate.memory.fact.repository.FactMapper; import vip.mate.workspace.document.WorkspaceFileService; import vip.mate.workspace.document.model.WorkspaceFileEntity; @@ -18,9 +20,10 @@ import java.util.List; * Rebuilds the fact projection from canonical sources. *

* Derived columns are overwritten; accumulated columns (use_count, last_used_at) - * are preserved via MERGE/upsert keyed on (agent_id, source_ref). + * are preserved via select-then-update keyed on (agent_id, source_ref). *

* Only this class may write derived columns to mate_fact (core invariant). + * Uses MyBatis Plus CRUD (dialect-safe for both H2 and MySQL). * * @author MateClaw Team */ @@ -65,13 +68,11 @@ public class FactProjectionBuilder { allFacts.addAll(extractor.extract(agentId, "MEMORY.md", memoryFile.getContent())); } - // Upsert all extracted facts + // Upsert all extracted facts (dialect-safe) LocalDateTime now = LocalDateTime.now(); List keepRefs = new ArrayList<>(); for (ExtractedFact fact : allFacts) { - factMapper.upsertDerivedH2(agentId, fact.sourceRef(), fact.category(), - fact.subject(), fact.predicate(), fact.objectValue(), - fact.confidence(), 0.5, fact.extractedBy(), now, now); + upsertDerived(agentId, fact, now); keepRefs.add(fact.sourceRef()); } @@ -93,11 +94,50 @@ public class FactProjectionBuilder { List facts = extractor.extract(agentId, filename, content); LocalDateTime now = LocalDateTime.now(); for (ExtractedFact fact : facts) { - factMapper.upsertDerivedH2(agentId, fact.sourceRef(), fact.category(), - fact.subject(), fact.predicate(), fact.objectValue(), - fact.confidence(), 0.5, fact.extractedBy(), now, now); + upsertDerived(agentId, fact, now); } log.debug("[FactProjection] rebuildOne: agent={}, file={}, facts={}", agentId, filename, facts.size()); return facts.size(); } + + /** + * Dialect-safe upsert: select by (agent_id, source_ref), then insert or update. + * Preserves accumulated columns (use_count, last_used_at) on update. + */ + private void upsertDerived(Long agentId, ExtractedFact fact, LocalDateTime now) { + FactEntity existing = factMapper.selectOne( + new LambdaQueryWrapper() + .eq(FactEntity::getAgentId, agentId) + .eq(FactEntity::getSourceRef, fact.sourceRef()) + .last("LIMIT 1")); + + if (existing != null) { + // Update derived columns only; preserve accumulated columns + existing.setCategory(fact.category()); + existing.setSubject(fact.subject()); + existing.setPredicate(fact.predicate()); + existing.setObjectValue(fact.objectValue()); + existing.setConfidence(fact.confidence()); + existing.setExtractedBy(fact.extractedBy()); + existing.setUpdateTime(now); + existing.setDeleted(0); // un-delete if previously soft-deleted + factMapper.updateById(existing); + } else { + FactEntity entity = new FactEntity(); + entity.setAgentId(agentId); + entity.setSourceRef(fact.sourceRef()); + entity.setCategory(fact.category()); + entity.setSubject(fact.subject()); + entity.setPredicate(fact.predicate()); + entity.setObjectValue(fact.objectValue()); + entity.setConfidence(fact.confidence()); + entity.setTrust(0.5); + entity.setUseCount(0); + entity.setExtractedBy(fact.extractedBy()); + entity.setCreateTime(now); + entity.setUpdateTime(now); + entity.setDeleted(0); + factMapper.insert(entity); + } + } } diff --git a/mateclaw-server/src/main/java/vip/mate/memory/fact/repository/FactMapper.java b/mateclaw-server/src/main/java/vip/mate/memory/fact/repository/FactMapper.java index ff257925..7a98302c 100644 --- a/mateclaw-server/src/main/java/vip/mate/memory/fact/repository/FactMapper.java +++ b/mateclaw-server/src/main/java/vip/mate/memory/fact/repository/FactMapper.java @@ -11,7 +11,7 @@ import java.util.List; /** * Fact mapper — limited write operations enforce the core invariant: - * - Derived columns: only FactProjectionBuilder may write + * - Derived columns: only FactProjectionBuilder may write (via upsertDerived) * - Accumulated columns: only bumpUseCount may write * * @author MateClaw Team @@ -19,29 +19,6 @@ import java.util.List; @Mapper public interface FactMapper extends BaseMapper { - /** - * Upsert derived columns by (agent_id, source_ref). - * Preserves accumulated columns (last_used_at, use_count). - */ - @Update(""" - MERGE INTO mate_fact (agent_id, source_ref, category, subject, predicate, object_value, - confidence, trust, extracted_by, create_time, update_time, deleted) - KEY (agent_id, source_ref) - VALUES (#{agentId}, #{sourceRef}, #{category}, #{subject}, #{predicate}, #{objectValue}, - #{confidence}, #{trust}, #{extractedBy}, #{createTime}, #{updateTime}, 0) - """) - void upsertDerivedH2(@Param("agentId") Long agentId, - @Param("sourceRef") String sourceRef, - @Param("category") String category, - @Param("subject") String subject, - @Param("predicate") String predicate, - @Param("objectValue") String objectValue, - @Param("confidence") Double confidence, - @Param("trust") Double trust, - @Param("extractedBy") String extractedBy, - @Param("createTime") LocalDateTime createTime, - @Param("updateTime") LocalDateTime updateTime); - /** * Bump use_count and last_used_at for the given fact IDs. * This is the ONLY path that writes accumulated columns. diff --git a/mateclaw-server/src/main/java/vip/mate/memory/service/MorningCardService.java b/mateclaw-server/src/main/java/vip/mate/memory/service/MorningCardService.java index f98ad412..1037d4ae 100644 --- a/mateclaw-server/src/main/java/vip/mate/memory/service/MorningCardService.java +++ b/mateclaw-server/src/main/java/vip/mate/memory/service/MorningCardService.java @@ -31,6 +31,7 @@ public class MorningCardService { * Get the morning card for a user+agent. Returns null if no unseen dream exists. */ public Map getCardFor(Long userId, Long agentId) { + if (userId == null || agentId == null) return null; // Find the latest successful dream report for this agent DreamReportEntity latestReport = dreamReportMapper.selectOne( new LambdaQueryWrapper() diff --git a/mateclaw-ui/src/i18n/locales/en-US.ts b/mateclaw-ui/src/i18n/locales/en-US.ts index 2da4fe27..09727652 100644 --- a/mateclaw-ui/src/i18n/locales/en-US.ts +++ b/mateclaw-ui/src/i18n/locales/en-US.ts @@ -1795,6 +1795,7 @@ export default { modeNightly: 'Nightly', candidates: 'candidates', loadMore: 'Load more', + retry: 'Retry', time: { justNow: 'just now', hoursAgo: '{n}h ago', diff --git a/mateclaw-ui/src/i18n/locales/zh-CN.ts b/mateclaw-ui/src/i18n/locales/zh-CN.ts index 1ca9dd2b..3928defd 100644 --- a/mateclaw-ui/src/i18n/locales/zh-CN.ts +++ b/mateclaw-ui/src/i18n/locales/zh-CN.ts @@ -1805,6 +1805,7 @@ export default { modeNightly: '夜间', candidates: '条候选', loadMore: '加载更多', + retry: '重试', time: { justNow: '刚刚', hoursAgo: '{n}小时前', diff --git a/mateclaw-ui/src/stores/useMemoryStore.ts b/mateclaw-ui/src/stores/useMemoryStore.ts index 203a4b31..4080a1b4 100644 --- a/mateclaw-ui/src/stores/useMemoryStore.ts +++ b/mateclaw-ui/src/stores/useMemoryStore.ts @@ -24,17 +24,23 @@ export const useMemoryStore = defineStore('memory', () => { const reports = ref([]) const total = ref(0) const loading = ref(false) + const error = ref(null) const currentReport = ref(null) - let eventSource: EventSource | null = null + let pollTimer: ReturnType | null = null async function fetchReports(agentId: number, page = 1, size = 20) { loading.value = true + error.value = null try { const res = await http.get(`/memory/${agentId}/dream/reports`, { params: { page, size }, }) reports.value = res.data.records || [] total.value = res.data.total || 0 + } catch (e: any) { + error.value = e.message || 'Failed to load reports' + reports.value = [] + total.value = 0 } finally { loading.value = false } @@ -45,46 +51,34 @@ export const useMemoryStore = defineStore('memory', () => { try { const res = await http.get(`/memory/${agentId}/dream/reports/${reportId}`) currentReport.value = res.data + } catch { + currentReport.value = null } finally { loading.value = false } } /** - * Subscribe to dream SSE events for an agent. - * Automatically refreshes the report list on new dream events. + * Poll for new dream events instead of SSE. + * SSE via EventSource doesn't support Authorization headers, + * causing 401 errors. Polling every 15s is sufficient for dream events. */ function subscribeEvents(agentId: number) { unsubscribeEvents() - const token = localStorage.getItem('token') - const url = `/api/v1/memory/${agentId}/dream/events` - eventSource = new EventSource(url) - - eventSource.addEventListener('dream.completed', (e) => { - // Refresh the report list to show the new dream + pollTimer = setInterval(() => { fetchReports(agentId, 1, 20) - }) - - eventSource.addEventListener('dream.failed', (e) => { - fetchReports(agentId, 1, 20) - }) - - eventSource.onerror = () => { - // Reconnect after 5s on error - unsubscribeEvents() - setTimeout(() => subscribeEvents(agentId), 5000) - } + }, 15000) } function unsubscribeEvents() { - if (eventSource) { - eventSource.close() - eventSource = null + if (pollTimer) { + clearInterval(pollTimer) + pollTimer = null } } return { - reports, total, loading, currentReport, + reports, total, loading, error, currentReport, fetchReports, fetchReport, subscribeEvents, unsubscribeEvents, } diff --git a/mateclaw-ui/src/views/Memory/index.vue b/mateclaw-ui/src/views/Memory/index.vue index 02cde980..0a035e21 100644 --- a/mateclaw-ui/src/views/Memory/index.vue +++ b/mateclaw-ui/src/views/Memory/index.vue @@ -63,6 +63,13 @@ +