212ff3ff80
Introduce UMS ingest/snapshot pipeline, Context Planner with multi-source recall, runtime context injection, canonical user mapping, and session snapshot loading on auth/me. Co-authored-by: Cursor <cursoragent@cursor.com>
111 lines
3.6 KiB
JavaScript
111 lines
3.6 KiB
JavaScript
import { buildContextPlan } from '../temporal-recall-service/context-planner.mjs';
|
|
import { queryTemporalRecall } from '../temporal-recall-service/recall.mjs';
|
|
import { resolveCanonicalUserId } from '../user-model-service/canonical-user.mjs';
|
|
|
|
function assertRouter(api) {
|
|
if (!api || typeof api.get !== 'function' || typeof api.post !== 'function') {
|
|
throw new Error('attachPortalTemporalRecallRoutes requires an Express-compatible router');
|
|
}
|
|
}
|
|
|
|
function parseBearerToken(req) {
|
|
const auth = req.headers.authorization ?? '';
|
|
return auth.startsWith('Bearer ') ? auth.slice(7).trim() : '';
|
|
}
|
|
|
|
function createResolveUser(getUserAuth) {
|
|
return async function resolveTemporalUser(req, _res, next) {
|
|
if (req.currentUser?.id) return next();
|
|
const bearer = parseBearerToken(req);
|
|
const userAuth = getUserAuth?.();
|
|
if (!bearer || !userAuth?.getMe) return next();
|
|
try {
|
|
const me = await userAuth.getMe(bearer);
|
|
if (me) req.currentUser = me;
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
return next();
|
|
};
|
|
}
|
|
|
|
function createRequireUser() {
|
|
return function requireUser(req, res, next) {
|
|
if (!req.currentUser?.id) {
|
|
return res.status(401).json({ message: '未授权,请重新登录' });
|
|
}
|
|
return next();
|
|
};
|
|
}
|
|
|
|
export function attachPortalTemporalRecallRoutes(api, { getPool, getUserAuth }) {
|
|
assertRouter(api);
|
|
const resolveTemporalUser = createResolveUser(getUserAuth);
|
|
const requireUser = createRequireUser();
|
|
|
|
api.use('/v1/context', resolveTemporalUser);
|
|
api.use('/v1/temporal-recall', resolveTemporalUser);
|
|
|
|
api.post('/v1/context/plan', requireUser, (req, res) => {
|
|
try {
|
|
const query = String(req.body?.query ?? '').trim();
|
|
if (!query) return res.status(400).json({ error: 'invalid_query' });
|
|
const plannerLevel = String(req.body?.planner_level ?? 'auto');
|
|
if (plannerLevel === 'semantic') {
|
|
return res.status(501).json({ error: 'not_implemented', message: 'semantic planner v0.2' });
|
|
}
|
|
const plan = buildContextPlan({
|
|
query,
|
|
user_id: resolveCanonicalUserId(req.currentUser.id),
|
|
now: req.body?.now ? new Date(req.body.now) : new Date(),
|
|
planner_level: plannerLevel,
|
|
});
|
|
return res.json({ plan });
|
|
} catch (err) {
|
|
if (err instanceof Error && err.message === 'invalid_query') {
|
|
return res.status(400).json({ error: 'invalid_query' });
|
|
}
|
|
console.error('[temporal] plan failed', err);
|
|
return res.status(500).json({ error: err instanceof Error ? err.message : 'plan failed' });
|
|
}
|
|
});
|
|
|
|
api.get('/v1/temporal-recall/info', (_req, res) => {
|
|
return res.json({
|
|
schema_version: 1,
|
|
supported_sources: ['calendar', 'meinput', 'chat'],
|
|
planned_sources: ['memory_v2', 'email', 'browser', 'tasks'],
|
|
temporal_modes: [
|
|
'OCCURRED_IN',
|
|
'MENTIONED_IN',
|
|
'PLANNED_IN',
|
|
'CREATED_IN',
|
|
'DUE_IN',
|
|
'AMBIGUOUS',
|
|
],
|
|
default_limit: 50,
|
|
max_limit: 200,
|
|
});
|
|
});
|
|
|
|
api.post('/v1/temporal-recall/query', requireUser, async (req, res) => {
|
|
try {
|
|
const pool = getPool?.();
|
|
const userId = resolveCanonicalUserId(req.currentUser.id);
|
|
const body = req.body ?? {};
|
|
const result = await queryTemporalRecall(pool, {
|
|
query: body.query,
|
|
plan: body.plan,
|
|
user_id: userId,
|
|
now: body.now ? new Date(body.now) : new Date(),
|
|
session_id: body.session_id,
|
|
limit: body.limit,
|
|
});
|
|
return res.json(result);
|
|
} catch (err) {
|
|
console.error('[temporal] query failed', err);
|
|
return res.status(500).json({ error: err instanceof Error ? err.message : 'query failed' });
|
|
}
|
|
});
|
|
}
|