mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
docs: update README title and tagline
This commit is contained in:
parent
d9d677e762
commit
c252cf50a4
@ -6,7 +6,7 @@
|
||||
|
||||
# MateClaw
|
||||
|
||||
<p align="center"><b>Your AI needs a Plan B.</b></p>
|
||||
<p align="center"><b>Your second brain</b></p>
|
||||
|
||||
[](https://github.com/matevip/mateclaw)
|
||||
[](https://claw.mate.vip/docs)
|
||||
|
||||
@ -4,9 +4,9 @@
|
||||
<img src="mateclaw-ui/public/logo/mateclaw_logo_s.png" alt="MateClaw Logo" width="120">
|
||||
</p>
|
||||
|
||||
# MateClaw
|
||||
# 太一(MateClaw)
|
||||
|
||||
<p align="center"><b>AI 也该有 Plan B。</b></p>
|
||||
<p align="center"><b>你的超级大脑</b></p>
|
||||
|
||||
[](https://github.com/matevip/mateclaw)
|
||||
[](https://claw.mate.vip/docs)
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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.
|
||||
* <p>
|
||||
* 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).
|
||||
* <p>
|
||||
* 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<String> 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<ExtractedFact> 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<FactEntity>()
|
||||
.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<FactEntity> {
|
||||
|
||||
/**
|
||||
* 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.
|
||||
|
||||
@ -31,6 +31,7 @@ public class MorningCardService {
|
||||
* Get the morning card for a user+agent. Returns null if no unseen dream exists.
|
||||
*/
|
||||
public Map<String, Object> 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<DreamReportEntity>()
|
||||
|
||||
@ -1795,6 +1795,7 @@ export default {
|
||||
modeNightly: 'Nightly',
|
||||
candidates: 'candidates',
|
||||
loadMore: 'Load more',
|
||||
retry: 'Retry',
|
||||
time: {
|
||||
justNow: 'just now',
|
||||
hoursAgo: '{n}h ago',
|
||||
|
||||
@ -1805,6 +1805,7 @@ export default {
|
||||
modeNightly: '夜间',
|
||||
candidates: '条候选',
|
||||
loadMore: '加载更多',
|
||||
retry: '重试',
|
||||
time: {
|
||||
justNow: '刚刚',
|
||||
hoursAgo: '{n}小时前',
|
||||
|
||||
@ -24,17 +24,23 @@ export const useMemoryStore = defineStore('memory', () => {
|
||||
const reports = ref<DreamReportItem[]>([])
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const currentReport = ref<DreamReportItem | null>(null)
|
||||
let eventSource: EventSource | null = null
|
||||
let pollTimer: ReturnType<typeof setInterval> | 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,
|
||||
}
|
||||
|
||||
@ -63,6 +63,13 @@
|
||||
<template v-if="store.loading">
|
||||
<div class="skeleton-card" v-for="i in 4" :key="i"><div class="skeleton-line" /><div class="skeleton-line short" /></div>
|
||||
</template>
|
||||
<template v-else-if="store.error">
|
||||
<div class="empty-state error-state">
|
||||
<div class="empty-icon">⚠️</div>
|
||||
<p>{{ store.error }}</p>
|
||||
<button class="retry-btn" @click="loadReports">{{ t('memory.retry') }}</button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="store.reports.length === 0">
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">🌙</div>
|
||||
@ -208,6 +215,7 @@ function selectAgent(agent: any) {
|
||||
selectedAgentId.value = agent.id
|
||||
agentDropdownOpen.value = false
|
||||
selectedReportId.value = null
|
||||
currentPage.value = 1
|
||||
store.currentReport = null
|
||||
}
|
||||
|
||||
@ -392,6 +400,12 @@ function fmtTime(iso: string) {
|
||||
.empty-state { display: flex; flex-direction: column; align-items: center; padding: 40px 0; color: var(--mc-text-tertiary); }
|
||||
.empty-icon { font-size: 28px; margin-bottom: 8px; }
|
||||
.empty-state p { font-size: 13px; }
|
||||
.error-state p { color: var(--mc-text-secondary); }
|
||||
.retry-btn {
|
||||
margin-top: 8px; padding: 6px 16px; border: 1px solid var(--mc-border); border-radius: 8px;
|
||||
background: transparent; font-size: 12px; color: var(--mc-text-secondary); cursor: pointer;
|
||||
}
|
||||
.retry-btn:hover { border-color: var(--mc-primary); color: var(--mc-primary); }
|
||||
|
||||
/* ========== Right detail ========== */
|
||||
.memory-detail {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user