feat(memory-v2): close Phase A with auto-review, product events, and H5 recall UI.
Add candidate auto-review pipeline, shadow audit tooling, admin metrics page, and user-visible memory recall hints in chat with phase-a readiness checks. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import { summarizePersonalMemoryObservation } from '../memory-v2-user-feedback.mjs';
|
||||
|
||||
function assertRouter(api) {
|
||||
if (
|
||||
!api ||
|
||||
@@ -11,11 +13,84 @@ function assertRouter(api) {
|
||||
}
|
||||
}
|
||||
|
||||
function parseRecallEventData(raw) {
|
||||
if (!raw) return null;
|
||||
if (typeof raw === 'object') return raw;
|
||||
try {
|
||||
return JSON.parse(String(raw));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function resolveLatestMemoryRecallHint(pool, { userId, sessionId }) {
|
||||
if (!pool?.query || !userId || !sessionId) {
|
||||
return {
|
||||
memoryCount: 0,
|
||||
injectionEnabled: false,
|
||||
mode: null,
|
||||
createdAt: null,
|
||||
savedPreview: null,
|
||||
memoryPreviews: [],
|
||||
};
|
||||
}
|
||||
const [rows] = await pool.query(
|
||||
`SELECT e.data_json, e.created_at, r.id AS run_id
|
||||
FROM h5_agent_run_events e
|
||||
INNER JOIN h5_agent_runs r ON r.id = e.run_id
|
||||
WHERE r.user_id = ?
|
||||
AND r.agent_session_id = ?
|
||||
AND e.event_type = 'agent_memory_resolved'
|
||||
ORDER BY e.created_at DESC
|
||||
LIMIT 1`,
|
||||
[String(userId), String(sessionId)],
|
||||
);
|
||||
const [candidateRows] = await pool.query(
|
||||
`SELECT content, updated_at
|
||||
FROM h5_memory_v2_candidates
|
||||
WHERE user_id = ?
|
||||
AND session_id = ?
|
||||
AND status = 'accepted'
|
||||
ORDER BY updated_at DESC
|
||||
LIMIT 1`,
|
||||
[String(userId), String(sessionId)],
|
||||
);
|
||||
const row = rows[0];
|
||||
const candidate = candidateRows[0];
|
||||
if (!row) {
|
||||
return {
|
||||
memoryCount: 0,
|
||||
injectionEnabled: false,
|
||||
mode: null,
|
||||
createdAt: null,
|
||||
runId: null,
|
||||
savedPreview: candidate?.content ? String(candidate.content).slice(0, 60) : null,
|
||||
savedAt: candidate?.updated_at == null ? null : Number(candidate.updated_at),
|
||||
memoryPreviews: [],
|
||||
};
|
||||
}
|
||||
const data = parseRecallEventData(row.data_json);
|
||||
const memoryPreviews = Array.isArray(data?.memoryPreviews)
|
||||
? data.memoryPreviews.map((item) => String(item).slice(0, 120)).filter(Boolean).slice(0, 5)
|
||||
: [];
|
||||
return {
|
||||
memoryCount: Number(data?.memoryCount ?? data?.count ?? 0) || 0,
|
||||
injectionEnabled: Boolean(data?.injectionEnabled),
|
||||
mode: data?.mode == null ? null : String(data.mode),
|
||||
createdAt: Number(row.created_at ?? 0) || null,
|
||||
runId: row.run_id == null ? null : String(row.run_id),
|
||||
savedPreview: candidate?.content ? String(candidate.content).slice(0, 60) : null,
|
||||
savedAt: candidate?.updated_at == null ? null : Number(candidate.updated_at),
|
||||
memoryPreviews,
|
||||
};
|
||||
}
|
||||
|
||||
export function attachPortalUserMemoryRoutes(
|
||||
api,
|
||||
{
|
||||
getMemoryV2 = () => null,
|
||||
getTkmindProxy = () => null,
|
||||
getPool = () => null,
|
||||
ensureUserMemoryCapability = async () => null,
|
||||
ownsAgentSession = async () => false,
|
||||
loadUserVisibleConversation = async () => [],
|
||||
@@ -64,12 +139,14 @@ export function attachPortalUserMemoryRoutes(
|
||||
sessionId,
|
||||
limit: 200,
|
||||
});
|
||||
const personalMemory = summarizePersonalMemoryObservation(result.personalMemory);
|
||||
return res.json({
|
||||
ok: true,
|
||||
analyzed: result.analyzed ?? 0,
|
||||
memories: result.memories ?? 0,
|
||||
totalMemories: memories.length,
|
||||
syncedToSession,
|
||||
personalMemory,
|
||||
});
|
||||
} catch (err) {
|
||||
return res.status(500).json({
|
||||
@@ -78,6 +155,37 @@ export function attachPortalUserMemoryRoutes(
|
||||
}
|
||||
});
|
||||
|
||||
api.get('/user-memory/v1/recall-hint', async (req, res) => {
|
||||
const memoryV2 = getMemoryV2();
|
||||
const memoryStatus = await memoryV2?.getStatus?.().catch(() => null);
|
||||
if (!memoryStatus?.enabled) {
|
||||
return res.status(503).json({ message: '长期记忆功能未启用' });
|
||||
}
|
||||
const capabilityState = await ensureUserMemoryCapability(req, res);
|
||||
if (!capabilityState) return;
|
||||
|
||||
const sessionId = String(req.query?.sessionId ?? '').trim();
|
||||
if (!sessionId) {
|
||||
return res.status(400).json({ message: '缺少 sessionId' });
|
||||
}
|
||||
const owns = await ownsAgentSession(req.currentUser.id, sessionId);
|
||||
if (!owns) {
|
||||
return res.status(403).json({ message: '无权访问该会话' });
|
||||
}
|
||||
|
||||
try {
|
||||
const hint = await resolveLatestMemoryRecallHint(getPool(), {
|
||||
userId: req.currentUser.id,
|
||||
sessionId,
|
||||
});
|
||||
return res.json({ ok: true, ...hint });
|
||||
} catch (err) {
|
||||
return res.status(500).json({
|
||||
message: err instanceof Error ? err.message : '读取记忆召回提示失败',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
api.post('/user-memory/v1/sync', async (req, res) => {
|
||||
const memoryV2 = getMemoryV2();
|
||||
const memoryStatus = await memoryV2?.getStatus?.().catch(() => null);
|
||||
|
||||
Reference in New Issue
Block a user