fix: 修复 Memory V2 合并导致的启动崩溃

master 的 Memory V2 重构删除了 memory/updater.py,admin_users.py 仍从中导入 get_memory_data 导致启动 ImportError。改用 BuiltinFileProvider 读取 V2 的 USER.md / 各 agent MEMORY.md;前端 MemoryBucketView 同步改为渲染 V2 条目列表。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
吉鑫升andClaude Opus 4.7 committed 2026-05-19 17:22:27 +08:00
1 parent 028c5a34e6
commit bf34b7df4e
2 files changed
+43 -56

No files matched your search

@@ -71,67 +71,38 @@ function MessageRow({ message }: { message: AdminMessage }) {
);
}
/** Render one memory bucket: context summaries + facts. */
/** Render one Memory V2 bucket: a flat list of USER.md / MEMORY.md entries. */
function MemoryBucketView({ bucket }: { bucket: AdminMemoryBucket }) {
const data = bucket.data as {
user?: Record<string, { summary?: string }>;
history?: Record<string, { summary?: string }>;
facts?: Array<{ content?: string; category?: string; confidence?: number }>;
};
const sections: Array<[string, string]> = [];
const labels: Record<string, string> = {
workContext: "工作背景",
personalContext: "个人背景",
topOfMind: "近期关注",
recentMonths: "近几个月",
earlierContext: "更早记录",
longTermBackground: "长期背景",
};
for (const group of [data.user, data.history]) {
for (const [key, value] of Object.entries(group ?? {})) {
const summary = value?.summary?.trim();
if (summary) sections.push([labels[key] ?? key, summary]);
}
}
const facts = (data.facts ?? []).filter((f) => f.content?.trim());
const raw = (bucket.data as { entries?: unknown }).entries;
const entries = Array.isArray(raw)
? raw.filter(
(e): e is string => typeof e === "string" && e.trim().length > 0,
)
: [];
return (
<div className="rounded-md border p-4">
<div className="mb-3 flex items-center gap-2">
<Badge variant={bucket.name === "global" ? "default" : "secondary"}>
{bucket.name === "global" ? "全局记忆" : `Agent: ${bucket.name}`}
{bucket.name === "global" ? "用户画像" : `Agent: ${bucket.name}`}
</Badge>
{entries.length > 0 && (
<span className="text-muted-foreground text-xs">
{entries.length} 条
</span>
)}
</div>
{sections.length === 0 && facts.length === 0 && (
{entries.length === 0 ? (
<p className="text-muted-foreground text-sm">该记忆库暂无内容。</p>
)}
{sections.length > 0 && (
<div className="flex flex-col gap-2">
{sections.map(([label, summary]) => (
<div key={label}>
<span className="text-muted-foreground text-xs">{label}</span>
<p className="text-sm">{summary}</p>
</div>
) : (
<ul className="flex flex-col gap-1.5">
{entries.map((entry, i) => (
<li key={i} className="flex items-start gap-2 text-sm">
<span className="text-muted-foreground">·</span>
<span className="flex-1 whitespace-pre-wrap">{entry}</span>
</li>
))}
</div>
)}
{facts.length > 0 && (
<div className="mt-3">
<span className="text-muted-foreground text-xs">事实记录({facts.length})</span>
<ul className="mt-1 flex flex-col gap-1">
{facts.map((fact, i) => (
<li key={i} className="flex items-start gap-2 text-sm">
<span className="text-muted-foreground">·</span>
<span className="flex-1">{fact.content}</span>
{fact.category && (
<Badge variant="outline" className="shrink-0 text-xs">
{fact.category}
</Badge>
)}
</li>
))}
</ul>
</div>
</ul>
)}
</div>
);
@@ -23,7 +23,7 @@ from app.gateway.deps import (
get_current_user_from_request,
get_thread_store,
)
from deerflow.agents.memory.updater import get_memory_data
from deerflow.agents.memory import BuiltinFileProvider
from deerflow.config.agents_config import AGENT_NAME_PATTERN
from deerflow.config.paths import get_paths
from deerflow.runtime import serialize_channel_values
@@ -136,24 +136,40 @@ class AdminMemoryResponse(BaseModel):
buckets: list[AdminMemoryBucket] = Field(default_factory=list)
def _read_memory_bucket(user_id: str, agent_id: str | None) -> dict:
"""Read one Memory V2 file into a JSON-able ``{"entries": [...]}`` dict.
``agent_id is None`` -> the user's ``USER.md`` (profile, cross-agent);
otherwise that agent's ``MEMORY.md`` (per-agent working notes).
"""
provider = BuiltinFileProvider()
provider.initialize(user_id=user_id, agent_id=agent_id)
target = "user" if agent_id is None else "memory"
return {"entries": provider.read(target)}
@router.get("/{user_id}/memory", response_model=AdminMemoryResponse)
async def get_user_memory(user_id: str, request: Request) -> AdminMemoryResponse:
"""Return every memory bucket (global + per-agent) for one user."""
"""Return every memory bucket for one user.
Memory V2 layout: the ``global`` bucket is ``USER.md`` (user profile);
each per-agent bucket is that agent's ``MEMORY.md`` working notes.
"""
await _require_admin(request)
buckets: list[AdminMemoryBucket] = [
AdminMemoryBucket(name="global", data=get_memory_data(None, user_id=user_id))
AdminMemoryBucket(name="global", data=_read_memory_bucket(user_id, None))
]
agents_root = get_paths().user_dir(user_id) / "agents"
if agents_root.is_dir():
for entry in sorted(agents_root.iterdir(), key=lambda p: p.name.lower()):
if not entry.is_dir() or not (entry / "memory.json").exists():
if not entry.is_dir() or not (entry / "MEMORY.md").exists():
continue
if not AGENT_NAME_PATTERN.match(entry.name):
continue
try:
data = get_memory_data(entry.name, user_id=user_id)
data = _read_memory_bucket(user_id, entry.name)
except Exception:
logger.warning("Failed to read memory bucket %s for user", entry.name)
continue