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 new file mode 100644 index 00000000..1f86a12c --- /dev/null +++ b/mateclaw-server/src/main/java/vip/mate/memory/controller/DreamController.java @@ -0,0 +1,69 @@ +package vip.mate.memory.controller; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; +import vip.mate.common.result.R; +import vip.mate.memory.model.DreamReportEntity; +import vip.mate.memory.repository.DreamReportMapper; +import vip.mate.workspace.core.annotation.RequireWorkspaceRole; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Dream report API — provides paginated access to dream history for the Memory Timeline UI. + * + * @author MateClaw Team + */ +@Tag(name = "Dream Reports") +@RestController +@RequestMapping("/api/v1/memory/{agentId}/dream") +@RequiredArgsConstructor +public class DreamController { + + private final DreamReportMapper dreamReportMapper; + + @Operation(summary = "List dream reports (paginated, newest first)") + @GetMapping("/reports") + @RequireWorkspaceRole("viewer") + public R> listReports( + @PathVariable Long agentId, + @RequestParam(defaultValue = "1") int page, + @RequestParam(defaultValue = "20") int size) { + Page pageParam = new Page<>(page, size); + Page result = dreamReportMapper.selectPage(pageParam, + new LambdaQueryWrapper() + .eq(DreamReportEntity::getAgentId, agentId) + .eq(DreamReportEntity::getDeleted, 0) + .orderByDesc(DreamReportEntity::getStartedAt)); + + Map data = new LinkedHashMap<>(); + data.put("records", result.getRecords()); + data.put("total", result.getTotal()); + data.put("page", result.getCurrent()); + data.put("size", result.getSize()); + return R.ok(data); + } + + @Operation(summary = "Get a single dream report by ID") + @GetMapping("/reports/{reportId}") + @RequireWorkspaceRole("viewer") + public R getReport( + @PathVariable Long agentId, + @PathVariable Long reportId) { + DreamReportEntity entity = dreamReportMapper.selectOne( + new LambdaQueryWrapper() + .eq(DreamReportEntity::getId, reportId) + .eq(DreamReportEntity::getAgentId, agentId) + .eq(DreamReportEntity::getDeleted, 0)); + if (entity == null) { + return R.fail("Report not found"); + } + return R.ok(entity); + } +} diff --git a/mateclaw-ui/src/i18n/locales/en-US.ts b/mateclaw-ui/src/i18n/locales/en-US.ts index 06c18fb7..236b02f5 100644 --- a/mateclaw-ui/src/i18n/locales/en-US.ts +++ b/mateclaw-ui/src/i18n/locales/en-US.ts @@ -237,6 +237,7 @@ export default { cronJobs: 'Cron Jobs', settings: 'Settings', logout: 'Logout', + memory: 'Memory', themeLight: 'Light', themeDark: 'Dark', themeSystem: 'System', @@ -1780,4 +1781,26 @@ export default { tokens: 'Tokens', }, }, + memory: { + title: 'Memory', + selectAgent: 'Select Agent', + selectAgentHint: 'Select an agent to view its memory', + tabTimeline: 'Dream Timeline', + tabMemory: 'Long-term Memory', + tabProfile: 'Profile', + tabFacts: 'Facts', + noReports: 'No dream reports yet', + report: { + mode: 'Mode', + topic: 'Topic', + status: 'Status', + time: 'Time', + candidates: 'Candidates', + promoted: 'Promoted', + rejected: 'Rejected', + trigger: 'Trigger', + reason: 'LLM Reason', + diff: 'Change Summary', + }, + }, } as const diff --git a/mateclaw-ui/src/i18n/locales/zh-CN.ts b/mateclaw-ui/src/i18n/locales/zh-CN.ts index cb858cd0..68bc99a3 100644 --- a/mateclaw-ui/src/i18n/locales/zh-CN.ts +++ b/mateclaw-ui/src/i18n/locales/zh-CN.ts @@ -239,6 +239,7 @@ export default { logout: '退出登录', themeLight: '浅色', themeDark: '深色', + memory: '记忆', themeSystem: '跟随系统', themeLabel: '外观', languageLabel: '语言', @@ -1790,4 +1791,26 @@ export default { tokens: 'Token', }, }, + memory: { + title: '记忆管理', + selectAgent: '选择 Agent', + selectAgentHint: '请先选择一个 Agent 查看其记忆', + tabTimeline: 'Dream 时间线', + tabMemory: '长期记忆', + tabProfile: '用户画像', + tabFacts: '事实库', + noReports: '暂无 Dream 记录', + report: { + mode: '模式', + topic: '主题', + status: '状态', + time: '时间', + candidates: '候选数', + promoted: '已整合', + rejected: '未采纳', + trigger: '触发方式', + reason: 'LLM 理由', + diff: '变更摘要', + }, + }, } as const diff --git a/mateclaw-ui/src/router/index.ts b/mateclaw-ui/src/router/index.ts index 322b0d4b..c7f58192 100644 --- a/mateclaw-ui/src/router/index.ts +++ b/mateclaw-ui/src/router/index.ts @@ -33,6 +33,12 @@ const router = createRouter({ component: () => import('@/views/Wiki/index.vue'), meta: { title: 'Wiki' }, }, + { + path: 'memory', + name: 'Memory', + component: () => import('@/views/Memory/index.vue'), + meta: { title: 'Memory' }, + }, // ==================== Connect ==================== { path: 'channels', diff --git a/mateclaw-ui/src/stores/useMemoryStore.ts b/mateclaw-ui/src/stores/useMemoryStore.ts new file mode 100644 index 00000000..79588eac --- /dev/null +++ b/mateclaw-ui/src/stores/useMemoryStore.ts @@ -0,0 +1,53 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import { http } from '@/api' + +export interface DreamReportItem { + id: string + agentId: number + mode: string + topic: string | null + triggerSource: string + triggeredBy: string + startedAt: string + finishedAt: string + candidateCount: number + promotedCount: number + rejectedCount: number + memoryDiff: string | null + llmReason: string | null + status: string + errorMessage: string | null +} + +export const useMemoryStore = defineStore('memory', () => { + const reports = ref([]) + const total = ref(0) + const loading = ref(false) + const currentReport = ref(null) + + async function fetchReports(agentId: number, page = 1, size = 20) { + loading.value = true + try { + const res = await http.get(`/memory/${agentId}/dream/reports`, { + params: { page, size }, + }) + reports.value = res.data.records || [] + total.value = res.data.total || 0 + } finally { + loading.value = false + } + } + + async function fetchReport(agentId: number, reportId: string) { + loading.value = true + try { + const res = await http.get(`/memory/${agentId}/dream/reports/${reportId}`) + currentReport.value = res.data + } finally { + loading.value = false + } + } + + return { reports, total, loading, currentReport, fetchReports, fetchReport } +}) diff --git a/mateclaw-ui/src/views/Memory/components/DreamReportPanel.vue b/mateclaw-ui/src/views/Memory/components/DreamReportPanel.vue new file mode 100644 index 00000000..fa63b037 --- /dev/null +++ b/mateclaw-ui/src/views/Memory/components/DreamReportPanel.vue @@ -0,0 +1,114 @@ + + + + + diff --git a/mateclaw-ui/src/views/Memory/components/DreamTimeline.vue b/mateclaw-ui/src/views/Memory/components/DreamTimeline.vue new file mode 100644 index 00000000..1c1607c3 --- /dev/null +++ b/mateclaw-ui/src/views/Memory/components/DreamTimeline.vue @@ -0,0 +1,167 @@ + + + + + diff --git a/mateclaw-ui/src/views/Memory/index.vue b/mateclaw-ui/src/views/Memory/index.vue new file mode 100644 index 00000000..3bdce9b2 --- /dev/null +++ b/mateclaw-ui/src/views/Memory/index.vue @@ -0,0 +1,72 @@ + + + + + diff --git a/mateclaw-ui/src/views/layout/MainLayout.vue b/mateclaw-ui/src/views/layout/MainLayout.vue index 0bb6c8e9..eeb48e41 100644 --- a/mateclaw-ui/src/views/layout/MainLayout.vue +++ b/mateclaw-ui/src/views/layout/MainLayout.vue @@ -306,6 +306,11 @@ const navGroups = computed(() => [ label: t('nav.wiki'), icon: ``, }, + { + path: '/memory', + label: t('nav.memory'), + icon: ``, + }, ], }, {