mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 03:55:09 +08:00
docs: update README title and tagline
This commit is contained in:
parent
d9d677e762
commit
c252cf50a4
@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
# MateClaw
|
# 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://github.com/matevip/mateclaw)
|
||||||
[](https://claw.mate.vip/docs)
|
[](https://claw.mate.vip/docs)
|
||||||
|
|||||||
@ -4,9 +4,9 @@
|
|||||||
<img src="mateclaw-ui/public/logo/mateclaw_logo_s.png" alt="MateClaw Logo" width="120">
|
<img src="mateclaw-ui/public/logo/mateclaw_logo_s.png" alt="MateClaw Logo" width="120">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
# MateClaw
|
# 太一(MateClaw)
|
||||||
|
|
||||||
<p align="center"><b>AI 也该有 Plan B。</b></p>
|
<p align="center"><b>你的超级大脑</b></p>
|
||||||
|
|
||||||
[](https://github.com/matevip/mateclaw)
|
[](https://github.com/matevip/mateclaw)
|
||||||
[](https://claw.mate.vip/docs)
|
[](https://claw.mate.vip/docs)
|
||||||
|
|||||||
@ -139,14 +139,13 @@ public class DreamController {
|
|||||||
|
|
||||||
private Long resolveUserId(Authentication auth) {
|
private Long resolveUserId(Authentication auth) {
|
||||||
if (auth == null) return null;
|
if (auth == null) return null;
|
||||||
// Resolve from auth principal — assumes user ID is accessible
|
|
||||||
try {
|
try {
|
||||||
Object principal = auth.getPrincipal();
|
Object principal = auth.getPrincipal();
|
||||||
if (principal instanceof vip.mate.auth.model.UserEntity user) {
|
if (principal instanceof vip.mate.auth.model.UserEntity user) {
|
||||||
return user.getId();
|
return user.getId();
|
||||||
}
|
}
|
||||||
// Fallback: use username hash as stable ID
|
// Fallback: use abs(hashCode) to avoid negative IDs, add offset to avoid collision with real IDs
|
||||||
return (long) auth.getName().hashCode();
|
return Math.abs((long) auth.getName().hashCode()) + 1_000_000_000L;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,13 @@
|
|||||||
package vip.mate.memory.fact.projection;
|
package vip.mate.memory.fact.projection;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import vip.mate.memory.MemoryProperties;
|
import vip.mate.memory.MemoryProperties;
|
||||||
import vip.mate.memory.fact.extraction.CompositeEntityExtractor;
|
import vip.mate.memory.fact.extraction.CompositeEntityExtractor;
|
||||||
import vip.mate.memory.fact.extraction.ExtractedFact;
|
import vip.mate.memory.fact.extraction.ExtractedFact;
|
||||||
|
import vip.mate.memory.fact.model.FactEntity;
|
||||||
import vip.mate.memory.fact.repository.FactMapper;
|
import vip.mate.memory.fact.repository.FactMapper;
|
||||||
import vip.mate.workspace.document.WorkspaceFileService;
|
import vip.mate.workspace.document.WorkspaceFileService;
|
||||||
import vip.mate.workspace.document.model.WorkspaceFileEntity;
|
import vip.mate.workspace.document.model.WorkspaceFileEntity;
|
||||||
@ -18,9 +20,10 @@ import java.util.List;
|
|||||||
* Rebuilds the fact projection from canonical sources.
|
* Rebuilds the fact projection from canonical sources.
|
||||||
* <p>
|
* <p>
|
||||||
* Derived columns are overwritten; accumulated columns (use_count, last_used_at)
|
* 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>
|
* <p>
|
||||||
* Only this class may write derived columns to mate_fact (core invariant).
|
* 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
|
* @author MateClaw Team
|
||||||
*/
|
*/
|
||||||
@ -65,13 +68,11 @@ public class FactProjectionBuilder {
|
|||||||
allFacts.addAll(extractor.extract(agentId, "MEMORY.md", memoryFile.getContent()));
|
allFacts.addAll(extractor.extract(agentId, "MEMORY.md", memoryFile.getContent()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upsert all extracted facts
|
// Upsert all extracted facts (dialect-safe)
|
||||||
LocalDateTime now = LocalDateTime.now();
|
LocalDateTime now = LocalDateTime.now();
|
||||||
List<String> keepRefs = new ArrayList<>();
|
List<String> keepRefs = new ArrayList<>();
|
||||||
for (ExtractedFact fact : allFacts) {
|
for (ExtractedFact fact : allFacts) {
|
||||||
factMapper.upsertDerivedH2(agentId, fact.sourceRef(), fact.category(),
|
upsertDerived(agentId, fact, now);
|
||||||
fact.subject(), fact.predicate(), fact.objectValue(),
|
|
||||||
fact.confidence(), 0.5, fact.extractedBy(), now, now);
|
|
||||||
keepRefs.add(fact.sourceRef());
|
keepRefs.add(fact.sourceRef());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,11 +94,50 @@ public class FactProjectionBuilder {
|
|||||||
List<ExtractedFact> facts = extractor.extract(agentId, filename, content);
|
List<ExtractedFact> facts = extractor.extract(agentId, filename, content);
|
||||||
LocalDateTime now = LocalDateTime.now();
|
LocalDateTime now = LocalDateTime.now();
|
||||||
for (ExtractedFact fact : facts) {
|
for (ExtractedFact fact : facts) {
|
||||||
factMapper.upsertDerivedH2(agentId, fact.sourceRef(), fact.category(),
|
upsertDerived(agentId, fact, now);
|
||||||
fact.subject(), fact.predicate(), fact.objectValue(),
|
|
||||||
fact.confidence(), 0.5, fact.extractedBy(), now, now);
|
|
||||||
}
|
}
|
||||||
log.debug("[FactProjection] rebuildOne: agent={}, file={}, facts={}", agentId, filename, facts.size());
|
log.debug("[FactProjection] rebuildOne: agent={}, file={}, facts={}", agentId, filename, facts.size());
|
||||||
return 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:
|
* 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
|
* - Accumulated columns: only bumpUseCount may write
|
||||||
*
|
*
|
||||||
* @author MateClaw Team
|
* @author MateClaw Team
|
||||||
@ -19,29 +19,6 @@ import java.util.List;
|
|||||||
@Mapper
|
@Mapper
|
||||||
public interface FactMapper extends BaseMapper<FactEntity> {
|
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.
|
* Bump use_count and last_used_at for the given fact IDs.
|
||||||
* This is the ONLY path that writes accumulated columns.
|
* 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.
|
* Get the morning card for a user+agent. Returns null if no unseen dream exists.
|
||||||
*/
|
*/
|
||||||
public Map<String, Object> getCardFor(Long userId, Long agentId) {
|
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
|
// Find the latest successful dream report for this agent
|
||||||
DreamReportEntity latestReport = dreamReportMapper.selectOne(
|
DreamReportEntity latestReport = dreamReportMapper.selectOne(
|
||||||
new LambdaQueryWrapper<DreamReportEntity>()
|
new LambdaQueryWrapper<DreamReportEntity>()
|
||||||
|
|||||||
@ -1795,6 +1795,7 @@ export default {
|
|||||||
modeNightly: 'Nightly',
|
modeNightly: 'Nightly',
|
||||||
candidates: 'candidates',
|
candidates: 'candidates',
|
||||||
loadMore: 'Load more',
|
loadMore: 'Load more',
|
||||||
|
retry: 'Retry',
|
||||||
time: {
|
time: {
|
||||||
justNow: 'just now',
|
justNow: 'just now',
|
||||||
hoursAgo: '{n}h ago',
|
hoursAgo: '{n}h ago',
|
||||||
|
|||||||
@ -1805,6 +1805,7 @@ export default {
|
|||||||
modeNightly: '夜间',
|
modeNightly: '夜间',
|
||||||
candidates: '条候选',
|
candidates: '条候选',
|
||||||
loadMore: '加载更多',
|
loadMore: '加载更多',
|
||||||
|
retry: '重试',
|
||||||
time: {
|
time: {
|
||||||
justNow: '刚刚',
|
justNow: '刚刚',
|
||||||
hoursAgo: '{n}小时前',
|
hoursAgo: '{n}小时前',
|
||||||
|
|||||||
@ -24,17 +24,23 @@ export const useMemoryStore = defineStore('memory', () => {
|
|||||||
const reports = ref<DreamReportItem[]>([])
|
const reports = ref<DreamReportItem[]>([])
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
const error = ref<string | null>(null)
|
||||||
const currentReport = ref<DreamReportItem | 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) {
|
async function fetchReports(agentId: number, page = 1, size = 20) {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
|
error.value = null
|
||||||
try {
|
try {
|
||||||
const res = await http.get(`/memory/${agentId}/dream/reports`, {
|
const res = await http.get(`/memory/${agentId}/dream/reports`, {
|
||||||
params: { page, size },
|
params: { page, size },
|
||||||
})
|
})
|
||||||
reports.value = res.data.records || []
|
reports.value = res.data.records || []
|
||||||
total.value = res.data.total || 0
|
total.value = res.data.total || 0
|
||||||
|
} catch (e: any) {
|
||||||
|
error.value = e.message || 'Failed to load reports'
|
||||||
|
reports.value = []
|
||||||
|
total.value = 0
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
@ -45,46 +51,34 @@ export const useMemoryStore = defineStore('memory', () => {
|
|||||||
try {
|
try {
|
||||||
const res = await http.get(`/memory/${agentId}/dream/reports/${reportId}`)
|
const res = await http.get(`/memory/${agentId}/dream/reports/${reportId}`)
|
||||||
currentReport.value = res.data
|
currentReport.value = res.data
|
||||||
|
} catch {
|
||||||
|
currentReport.value = null
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subscribe to dream SSE events for an agent.
|
* Poll for new dream events instead of SSE.
|
||||||
* Automatically refreshes the report list on new dream events.
|
* SSE via EventSource doesn't support Authorization headers,
|
||||||
|
* causing 401 errors. Polling every 15s is sufficient for dream events.
|
||||||
*/
|
*/
|
||||||
function subscribeEvents(agentId: number) {
|
function subscribeEvents(agentId: number) {
|
||||||
unsubscribeEvents()
|
unsubscribeEvents()
|
||||||
const token = localStorage.getItem('token')
|
pollTimer = setInterval(() => {
|
||||||
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
|
|
||||||
fetchReports(agentId, 1, 20)
|
fetchReports(agentId, 1, 20)
|
||||||
})
|
}, 15000)
|
||||||
|
|
||||||
eventSource.addEventListener('dream.failed', (e) => {
|
|
||||||
fetchReports(agentId, 1, 20)
|
|
||||||
})
|
|
||||||
|
|
||||||
eventSource.onerror = () => {
|
|
||||||
// Reconnect after 5s on error
|
|
||||||
unsubscribeEvents()
|
|
||||||
setTimeout(() => subscribeEvents(agentId), 5000)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function unsubscribeEvents() {
|
function unsubscribeEvents() {
|
||||||
if (eventSource) {
|
if (pollTimer) {
|
||||||
eventSource.close()
|
clearInterval(pollTimer)
|
||||||
eventSource = null
|
pollTimer = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
reports, total, loading, currentReport,
|
reports, total, loading, error, currentReport,
|
||||||
fetchReports, fetchReport,
|
fetchReports, fetchReport,
|
||||||
subscribeEvents, unsubscribeEvents,
|
subscribeEvents, unsubscribeEvents,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -63,6 +63,13 @@
|
|||||||
<template v-if="store.loading">
|
<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>
|
<div class="skeleton-card" v-for="i in 4" :key="i"><div class="skeleton-line" /><div class="skeleton-line short" /></div>
|
||||||
</template>
|
</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">
|
<template v-else-if="store.reports.length === 0">
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<div class="empty-icon">🌙</div>
|
<div class="empty-icon">🌙</div>
|
||||||
@ -208,6 +215,7 @@ function selectAgent(agent: any) {
|
|||||||
selectedAgentId.value = agent.id
|
selectedAgentId.value = agent.id
|
||||||
agentDropdownOpen.value = false
|
agentDropdownOpen.value = false
|
||||||
selectedReportId.value = null
|
selectedReportId.value = null
|
||||||
|
currentPage.value = 1
|
||||||
store.currentReport = null
|
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-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-icon { font-size: 28px; margin-bottom: 8px; }
|
||||||
.empty-state p { font-size: 13px; }
|
.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 ========== */
|
/* ========== Right detail ========== */
|
||||||
.memory-detail {
|
.memory-detail {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user