From 0edfc948ba42e02cf8efd79c893e94641415e997 Mon Sep 17 00:00:00 2001 From: Blizzard Date: Mon, 22 Jun 2026 14:48:11 +0800 Subject: [PATCH] =?UTF-8?q?feat(memory):=20P2=20=E8=AF=BB=E8=B7=AF?= =?UTF-8?q?=E5=BE=84=E6=89=93=E5=88=86=20+=20=E6=A1=8C=E9=9D=A2=E7=AB=AF?= =?UTF-8?q?=E8=AE=B0=E5=BF=86=E9=9D=A2=E6=9D=BF=EF=BC=88=E7=9C=8B/?= =?UTF-8?q?=E6=94=B9/=E5=88=A0=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 读路径(Generative Agents 公式的 Recency+Importance 两项,Relevance 待 P3): - memory.Get 改为 rankProfiles:Score=0.4·Recency+0.6·Importance,按分降序、截断 top-30。 Recency=0.98^天 指数衰减;未评分行用兜底 importance=5(不被不公平遗忘)。纯函数 + 单测。 - 新增 Store.List(结构化、不截断)+ memory_list 工具。 桌面端记忆面板: - gateway GET /memory(列表) + DELETE /memory?key=(软删),受保护组。 - api listMemory/deleteMemory;MemoryView 右侧从占位 → 真列表:按分排序展示 key/value/重要度/最近时间,可内联编辑(PUT)与删除(软删);左侧登记后自动刷新。 实测:PUT 两条 → GET 返回带 importance/last_seen 的有序列表(较新者靠前)→ DELETE 一条 (软删)→ 再 GET 已消失。rank/recency/兜底 importance 单测过;前端 tsc+构建通过。 至此记忆:召回(打分) + 历史 + 自动对账(P1) + 用户可管控(面板) 闭环。Relevance(Milvus) 留 P3。 Co-Authored-By: Claude Opus 4.8 --- PROGRESS.md | 1 + sundynix-desktop/frontend/src/lib/api.ts | 25 ++++ .../frontend/src/panels/MemoryPanel.tsx | 3 +- .../frontend/src/views/MemoryView.tsx | 125 ++++++++++++++++-- .../internal/handler/task_handler.go | 33 +++++ sundynix-gateway/internal/router/router.go | 2 + sundynix-mcp-go/internal/mcp/gateway.go | 24 ++++ sundynix-mcp-go/internal/memory/rank_test.go | 50 +++++++ sundynix-mcp-go/internal/memory/store.go | 67 +++++++++- 9 files changed, 316 insertions(+), 14 deletions(-) create mode 100644 sundynix-mcp-go/internal/memory/rank_test.go diff --git a/PROGRESS.md b/PROGRESS.md index c6823ef..07c57e3 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -58,6 +58,7 @@ - [x] Harness 熔断降级中心(真三态状态机 Closed/Open/HalfOpen + 单测含 -race;熔断时回流提示并收尾流,不静默丢弃) - [x] Harness LLM 自动化评测(规则检查 + LLM-as-judge,异步 off 热路径评分记录 + 单测) - [x] 长期记忆 P1:异步攒批 Consolidate(每3轮1次 LLM 对账→ADD/UPDATE/DELETE/NOOP)+ 软删 + Profile 加 importance/last_seen(为 Generative Agents 打分铺路);纯逻辑+store集成单测 +- [x] 长期记忆 P2:读路径按 Score(Recency+Importance) 排序+衰减+截断top30(rank单测);桌面端记忆面板可看/改/删(软删);memory_list/delete 工具+端点;🟡 Relevance(Milvus)待P3 ## 第 5 层 · MCP TOOLS diff --git a/sundynix-desktop/frontend/src/lib/api.ts b/sundynix-desktop/frontend/src/lib/api.ts index 8dfd8c6..ff83de4 100644 --- a/sundynix-desktop/frontend/src/lib/api.ts +++ b/sundynix-desktop/frontend/src/lib/api.ts @@ -411,3 +411,28 @@ export async function setMemory( if (!res.ok) throw new Error(data.error ?? `memory failed: ${res.status}`); return data.message ?? "ok"; } + +// 一条长期偏好(含读路径打分维度)。 +export interface MemoryItem { + key: string; + value: string; + importance: number; + last_seen: string; +} + +// listMemory: GET /api/v1/memory —— 列出当前用户偏好(已按 Score 降序)。 +export async function listMemory(id: Identity): Promise { + const res = guard401(await fetch(`${GATEWAY}/api/v1/memory`, { headers: idHeaders(id) })); + const data = (await res.json()) as { memories?: MemoryItem[]; error?: string }; + if (!res.ok) throw new Error(data.error ?? `list memory failed: ${res.status}`); + return data.memories ?? []; +} + +// deleteMemory: DELETE /api/v1/memory?key= —— 软删一条偏好。 +export async function deleteMemory(id: Identity, key: string): Promise { + const res = guard401(await fetch(`${GATEWAY}/api/v1/memory?key=${encodeURIComponent(key)}`, { method: "DELETE", headers: idHeaders(id) })); + if (!res.ok) { + const d = (await res.json().catch(() => ({}))) as { error?: string }; + throw new Error(d.error ?? `delete memory failed: ${res.status}`); + } +} diff --git a/sundynix-desktop/frontend/src/panels/MemoryPanel.tsx b/sundynix-desktop/frontend/src/panels/MemoryPanel.tsx index 4e0aa12..9aea5fa 100644 --- a/sundynix-desktop/frontend/src/panels/MemoryPanel.tsx +++ b/sundynix-desktop/frontend/src/panels/MemoryPanel.tsx @@ -4,7 +4,7 @@ import { setMemory, type Identity } from "../lib/api"; import { Button, Input, Textarea, Field, useToast } from "../ui"; // 偏好记忆面板 —— 让用户显式登记/纠正模型对自己的记忆(→ PUT /api/v1/memory)。 -export function MemoryPanel({ identity }: { identity: Identity }) { +export function MemoryPanel({ identity, onSaved }: { identity: Identity; onSaved?: () => void }) { const toast = useToast(); const [key, setKey] = useState("回答偏好"); const [value, setValue] = useState("简洁、中文、多给要点"); @@ -16,6 +16,7 @@ export function MemoryPanel({ identity }: { identity: Identity }) { const m = await setMemory(identity, key.trim(), value.trim()); setSaved((s) => [{ key: key.trim(), value: value.trim() }, ...s.filter((x) => x.key !== key.trim())]); toast.push("success", m); + onSaved?.(); } catch (e) { toast.push("error", (e as Error).message); } diff --git a/sundynix-desktop/frontend/src/views/MemoryView.tsx b/sundynix-desktop/frontend/src/views/MemoryView.tsx index 4bd861e..c1b57ee 100644 --- a/sundynix-desktop/frontend/src/views/MemoryView.tsx +++ b/sundynix-desktop/frontend/src/views/MemoryView.tsx @@ -1,18 +1,123 @@ +import { useCallback, useEffect, useState } from "react"; +import { Trash2, RefreshCw, Save, X, Pencil, Brain } from "lucide-react"; import { MemoryPanel } from "../panels/MemoryPanel"; -import type { Identity } from "../lib/api"; +import { listMemory, setMemory, deleteMemory, type Identity, type MemoryItem } from "../lib/api"; +import { Textarea, EmptyState, useToast, cn } from "../ui"; -// 记忆模块:当前以偏好登记为主;规划做厚为 画像看/改/删 + 会话历史浏览。 +// 记忆模块:左侧登记偏好;右侧"模型记得我什么"——按 Score 排序列出,可改/删(软删)。 export function MemoryView({ identity }: { identity: Identity }) { + const toast = useToast(); + const [items, setItems] = useState([]); + const [loading, setLoading] = useState(false); + const [editKey, setEditKey] = useState(null); + const [draft, setDraft] = useState(""); + + const load = useCallback(async () => { + setLoading(true); + try { + setItems(await listMemory(identity)); + } catch { + setItems([]); + } finally { + setLoading(false); + } + }, [identity]); + useEffect(() => { + void load(); + }, [load]); + + const saveEdit = async (key: string) => { + if (!draft.trim()) return; + try { + await setMemory(identity, key, draft.trim()); + setEditKey(null); + toast.push("success", "已更新"); + void load(); + } catch (e) { + toast.push("error", (e as Error).message); + } + }; + + const remove = async (key: string) => { + try { + await deleteMemory(identity, key); + toast.push("success", `已删除「${key}」`); + void load(); + } catch (e) { + toast.push("error", (e as Error).message); + } + }; + + const fmtDate = (s: string) => { + const d = new Date(s); + return isNaN(d.getTime()) || d.getFullYear() < 2000 ? "—" : d.toLocaleDateString(); + }; + return ( -
-
- +
+
+
-
-
模型记得我什么 / 会话历史
- 规划:长期画像逐条列出可改/删(含来源),会话列表 + 多轮历史浏览与清空,注入开关。 -
- 当前左侧可登记偏好(→ memory_upsert → sundynix_user_profile)。 +
+
+ +

模型记得我什么({items.length})

+ 按重要度 × 最近性排序 + +
+ + {items.length === 0 ? ( + + ) : ( +
    + {items.map((m) => ( +
  • +
    + {m.key} + + 重要度 {m.importance || "—"} + + 最近 {fmtDate(m.last_seen)} +
    + {editKey === m.key ? ( + <> + + + + ) : ( + <> + + + + )} +
    +
    + {editKey === m.key ? ( +