b2a5caf67d
Lower term threshold, tune promotion scores, project candidate fallback in snapshot, and add rebuild-ums-snapshot script for production backfill. Co-authored-by: Cursor <cursoragent@cursor.com>
229 lines
6.9 KiB
JavaScript
229 lines
6.9 KiB
JavaScript
import crypto from 'node:crypto';
|
|
|
|
function newId() {
|
|
return crypto.randomUUID();
|
|
}
|
|
|
|
function nowMs() {
|
|
return Date.now();
|
|
}
|
|
|
|
function hashJson(obj) {
|
|
return crypto.createHash('sha256').update(JSON.stringify(obj)).digest('hex');
|
|
}
|
|
|
|
/**
|
|
* @param {import('mysql2/promise').Pool} pool
|
|
* @param {string} userId
|
|
* @param {object} options
|
|
*/
|
|
export async function materializeProfileAndSnapshot(pool, userId, options = {}) {
|
|
const projection = options.projection ?? 'default';
|
|
const reason = options.reason ?? 'candidate_accepted';
|
|
const ts = nowMs();
|
|
|
|
const [projects] = await pool.query(
|
|
`SELECT e.canonical_name, a.confidence, a.effective_weight, a.last_seen_at
|
|
FROM um_entities e
|
|
JOIN um_attributes a ON a.entity_id = e.entity_id AND a.status = 'active'
|
|
WHERE e.user_id = ? AND e.entity_type = 'project' AND e.status = 'active'
|
|
ORDER BY a.effective_weight DESC
|
|
LIMIT 8`,
|
|
[userId],
|
|
);
|
|
|
|
const [focusCandidates] = await pool.query(
|
|
`SELECT hypothesis_json, confidence, promotion_score, last_seen_at
|
|
FROM um_candidates
|
|
WHERE user_id = ? AND candidate_type = 'focus' AND status IN ('open', 'accepted')
|
|
ORDER BY promotion_score DESC
|
|
LIMIT 10`,
|
|
[userId],
|
|
);
|
|
|
|
const [projectCandidates] = await pool.query(
|
|
`SELECT hypothesis_json, confidence, promotion_score, last_seen_at
|
|
FROM um_candidates
|
|
WHERE user_id = ? AND candidate_type = 'project' AND status IN ('open', 'accepted', 'observed')
|
|
ORDER BY promotion_score DESC
|
|
LIMIT 8`,
|
|
[userId],
|
|
);
|
|
|
|
const [versionRows] = await pool.query(
|
|
`SELECT COALESCE(MAX(profile_version), 0) AS v FROM um_profile_versions WHERE user_id = ?`,
|
|
[userId],
|
|
);
|
|
const profileVersion = Number(versionRows[0]?.v ?? 0) + 1;
|
|
|
|
const activeProjects = projects.map((row) => ({
|
|
id: `project_${String(row.canonical_name).toLowerCase().replace(/[^a-z0-9]+/g, '_')}`,
|
|
name: row.canonical_name,
|
|
status: 'active',
|
|
confidence: Number(row.confidence),
|
|
last_seen: row.last_seen_at,
|
|
}));
|
|
|
|
if (activeProjects.length === 0 && projectCandidates.length > 0) {
|
|
for (const row of projectCandidates) {
|
|
const h = typeof row.hypothesis_json === 'string' ? JSON.parse(row.hypothesis_json) : row.hypothesis_json;
|
|
const name = h.name ?? h.topic ?? 'unknown';
|
|
activeProjects.push({
|
|
id: `project_${String(name).toLowerCase().replace(/[^a-z0-9]+/g, '_')}`,
|
|
name,
|
|
status: h.status ?? 'active',
|
|
confidence: Number(row.confidence),
|
|
last_seen: row.last_seen_at,
|
|
});
|
|
}
|
|
}
|
|
|
|
const recentFocus = focusCandidates.map((row) => {
|
|
const h = typeof row.hypothesis_json === 'string' ? JSON.parse(row.hypothesis_json) : row.hypothesis_json;
|
|
return {
|
|
topic: h.topic ?? h.name ?? 'unknown',
|
|
weight: Number(row.promotion_score),
|
|
ttl_days: 14,
|
|
};
|
|
});
|
|
|
|
const structured = {
|
|
identity: [],
|
|
active_projects: activeProjects,
|
|
recent_focus: recentFocus,
|
|
technical_preferences: [],
|
|
working_style: [],
|
|
};
|
|
|
|
const structuredHash = hashJson(structured);
|
|
await pool.query(
|
|
`UPDATE um_profile_versions SET status = 'superseded' WHERE user_id = ? AND status = 'active'`,
|
|
[userId],
|
|
);
|
|
await pool.query(
|
|
`INSERT INTO um_profile_versions
|
|
(profile_version, user_id, structured_json, content_hash, parent_version, materialize_reason, created_at, status)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, 'active')`,
|
|
[
|
|
profileVersion,
|
|
userId,
|
|
JSON.stringify(structured),
|
|
structuredHash,
|
|
profileVersion > 1 ? profileVersion - 1 : null,
|
|
reason,
|
|
ts,
|
|
],
|
|
);
|
|
|
|
const [fastRows] = await pool.query(
|
|
`SELECT COALESCE(MAX(fast_revision), 0) AS r FROM um_profile_snapshots WHERE user_id = ? AND projection = ?`,
|
|
[userId, projection],
|
|
);
|
|
const fastRevision = Number(fastRows[0]?.r ?? 0) + 1;
|
|
|
|
const agentHints = [];
|
|
if (activeProjects[0]) agentHints.push(`近期重点:${activeProjects[0].name}`);
|
|
if (recentFocus[0]) agentHints.push(`关注话题:${recentFocus[0].topic}`);
|
|
|
|
const snapshotCore = {
|
|
identity: structured.identity,
|
|
active_projects: activeProjects,
|
|
recent_focus: recentFocus,
|
|
technical_preferences: structured.technical_preferences,
|
|
working_style: structured.working_style,
|
|
agent_hints: agentHints,
|
|
};
|
|
|
|
const snapshotBody = {
|
|
profile_version: profileVersion,
|
|
fast_revision: fastRevision,
|
|
projection,
|
|
core: snapshotCore,
|
|
meta: {
|
|
graph_entity_count: activeProjects.length,
|
|
open_candidates: recentFocus.length,
|
|
},
|
|
};
|
|
|
|
const snapshotJson = {
|
|
...snapshotBody,
|
|
stale_after_sec: 3600,
|
|
};
|
|
const snapshotStr = JSON.stringify(snapshotCore);
|
|
const byteSize = Buffer.byteLength(snapshotStr, 'utf8');
|
|
const contentHash = hashJson(snapshotBody);
|
|
|
|
await pool.query(
|
|
`UPDATE um_profile_snapshots SET status = 'superseded' WHERE user_id = ? AND projection = ? AND status = 'active'`,
|
|
[userId, projection],
|
|
);
|
|
|
|
const snapshotId = newId();
|
|
await pool.query(
|
|
`INSERT INTO um_profile_snapshots
|
|
(snapshot_id, user_id, projection, profile_version, fast_revision, snapshot_json, byte_size,
|
|
content_hash, created_at, status)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'active')`,
|
|
[
|
|
snapshotId,
|
|
userId,
|
|
projection,
|
|
profileVersion,
|
|
fastRevision,
|
|
JSON.stringify(snapshotJson),
|
|
byteSize,
|
|
contentHash,
|
|
ts,
|
|
],
|
|
);
|
|
|
|
return {
|
|
profile_version: profileVersion,
|
|
fast_revision: fastRevision,
|
|
snapshot_id: snapshotId,
|
|
content_hash: contentHash,
|
|
byte_size: byteSize,
|
|
};
|
|
}
|
|
|
|
export async function getActiveSnapshot(pool, userId, projection = 'default') {
|
|
const [rows] = await pool.query(
|
|
`SELECT snapshot_id, user_id, projection, profile_version, fast_revision, snapshot_json,
|
|
byte_size, content_hash, created_at
|
|
FROM um_profile_snapshots
|
|
WHERE user_id = ? AND projection = ? AND status = 'active'
|
|
ORDER BY created_at DESC
|
|
LIMIT 1`,
|
|
[userId, projection],
|
|
);
|
|
const row = rows[0];
|
|
if (!row) return null;
|
|
const snapshotJson =
|
|
typeof row.snapshot_json === 'string' ? JSON.parse(row.snapshot_json) : row.snapshot_json;
|
|
return {
|
|
snapshot_id: row.snapshot_id,
|
|
user_id: row.user_id,
|
|
projection: row.projection,
|
|
profile_version: row.profile_version,
|
|
fast_revision: row.fast_revision,
|
|
content_hash: row.content_hash,
|
|
byte_size: row.byte_size,
|
|
stale_after_sec: snapshotJson.stale_after_sec ?? 3600,
|
|
core: snapshotJson.core ?? snapshotJson,
|
|
meta: snapshotJson.meta ?? {},
|
|
created_at: row.created_at,
|
|
};
|
|
}
|
|
|
|
export async function getSnapshotInfo(pool, userId, projection = 'default') {
|
|
const snap = await getActiveSnapshot(pool, userId, projection);
|
|
if (!snap) return null;
|
|
return {
|
|
profile_version: snap.profile_version,
|
|
fast_revision: snap.fast_revision,
|
|
content_hash: snap.content_hash,
|
|
projection: snap.projection,
|
|
snapshot_id: snap.snapshot_id,
|
|
};
|
|
}
|