Files
memind/server/portal-goal-run-routes.mjs
john 666db0b939 feat(goal-run): add multi-checkpoint goal orchestration with H5 and admin surfaces.
Persist goal runs in MySQL, bind agent runs to checkpoints, expose awaiting-approval
UX in chat, and add admin inspection routes with local verify scripts.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-01 17:03:16 +08:00

240 lines
7.3 KiB
JavaScript

import { isGoalRunEnabledForUser, buildGoalContinueUserMessage } from '../goal-run-intent.mjs';
import { resolveGoalBindingForAgentRun } from '../goal-run-resolve.mjs';
import crypto from 'node:crypto';
function assertRouter(api) {
if (
!api ||
typeof api.get !== 'function' ||
typeof api.post !== 'function' ||
typeof api.delete !== 'function'
) {
throw new Error(
'attachPortalGoalRunRoutes requires an Express-compatible router',
);
}
}
function goalRunUnavailable(res) {
return res.status(503).json({ message: 'Goal Run 功能未启用' });
}
function goalRunForbidden(res) {
return res.status(403).json({ message: '当前用户未开启 Goal Run' });
}
function parseStatuses(raw) {
const value = String(raw ?? '').trim();
if (!value) return ['active', 'awaiting_user', 'paused'];
return value.split(',').map((item) => item.trim()).filter(Boolean);
}
export function attachPortalGoalRunRoutes(
api,
{
getGoalRunService = () => null,
getAgentRunGateway = () => null,
env = process.env,
} = {},
) {
assertRouter(api);
function ensureGoalRunAccess(req, res) {
const service = getGoalRunService();
if (!service) {
goalRunUnavailable(res);
return null;
}
if (!isGoalRunEnabledForUser(req.currentUser?.id, env)) {
goalRunForbidden(res);
return null;
}
return service;
}
api.post('/goals', async (req, res) => {
try {
const service = ensureGoalRunAccess(req, res);
if (!service) return;
const title = String(req.body?.title ?? '').trim();
const intentSummary = String(
req.body?.intentSummary ?? req.body?.intent_summary ?? '',
).trim();
if (!title || !intentSummary) {
return res.status(400).json({ message: '缺少 title 或 intentSummary' });
}
const checkpoints = Array.isArray(req.body?.checkpoints) ? req.body.checkpoints : [];
const goal = await service.createGoalRun({
userId: req.currentUser.id,
title,
intentSummary,
sourceChannel: String(req.body?.sourceChannel ?? req.body?.source_channel ?? 'api'),
sourceSessionId: req.body?.sourceSessionId ?? req.body?.source_session_id ?? null,
sourceMessageId: req.body?.sourceMessageId ?? req.body?.source_message_id ?? null,
checkpoints,
context: req.body?.context ?? null,
memorySnapshot: req.body?.memorySnapshot ?? req.body?.memory_snapshot ?? null,
});
return res.status(201).json({ goal });
} catch (err) {
return res.status(500).json({
message: err instanceof Error ? err.message : '创建目标失败',
});
}
});
api.get('/goals', async (req, res) => {
try {
const service = ensureGoalRunAccess(req, res);
if (!service) return;
const goals = await service.listGoalRuns({
userId: req.currentUser.id,
statuses: parseStatuses(req.query?.status ?? req.query?.statuses),
limit: Number(req.query?.limit ?? 20),
});
return res.json({ goals });
} catch (err) {
return res.status(500).json({
message: err instanceof Error ? err.message : '读取目标列表失败',
});
}
});
api.get('/goals/:goalRunId', async (req, res) => {
try {
const service = ensureGoalRunAccess(req, res);
if (!service) return;
const goal = await service.getGoalRun({
userId: req.currentUser.id,
goalRunId: req.params.goalRunId,
});
if (!goal) {
return res.status(404).json({ message: '目标不存在' });
}
return res.json({ goal });
} catch (err) {
return res.status(500).json({
message: err instanceof Error ? err.message : '读取目标详情失败',
});
}
});
api.post('/goals/:goalRunId/checkpoints/:checkpointId/approve', async (req, res) => {
try {
const service = ensureGoalRunAccess(req, res);
if (!service) return;
const goal = await service.approveCheckpoint({
userId: req.currentUser.id,
goalRunId: req.params.goalRunId,
checkpointId: req.params.checkpointId,
feedback: req.body?.feedback ?? null,
});
const sessionId = String(
req.body?.sessionId ?? req.body?.session_id ?? '',
).trim() || null;
let run = null;
const gateway = getAgentRunGateway?.();
if (sessionId && gateway?.createRun) {
const userMessage = buildGoalContinueUserMessage(req.body?.feedback ?? null);
const requestId = String(
req.body?.request_id ?? req.body?.requestId ?? crypto.randomUUID(),
).trim();
const binding = await resolveGoalBindingForAgentRun({
goalRunService: service,
userId: req.currentUser.id,
userMessage,
sessionId,
body: { goal_run_id: req.params.goalRunId },
env,
});
if (binding?.goalRunId) {
run = await gateway.createRun(req.currentUser.id, {
sessionId,
requestId,
userMessage,
goalRunId: binding.goalRunId,
goalCheckpointId: binding.goalCheckpointId,
});
}
}
return res.json({ goal, ...(run ? { run } : {}) });
} catch (err) {
const status = err?.code === 'GOAL_RUN_NOT_FOUND' || err?.code === 'GOAL_CHECKPOINT_NOT_FOUND'
? 404
: 500;
return res.status(status).json({
message: err instanceof Error ? err.message : '确认阶段失败',
code: err?.code ?? null,
});
}
});
api.post('/goals/:goalRunId/pause', async (req, res) => {
try {
const service = ensureGoalRunAccess(req, res);
if (!service) return;
const goal = await service.pauseGoal({
userId: req.currentUser.id,
goalRunId: req.params.goalRunId,
});
return res.json({ goal });
} catch (err) {
const status = err?.code === 'GOAL_RUN_NOT_PAUSABLE' ? 409 : 500;
return res.status(status).json({
message: err instanceof Error ? err.message : '暂停目标失败',
code: err?.code ?? null,
});
}
});
api.post('/goals/:goalRunId/resume', async (req, res) => {
try {
const service = ensureGoalRunAccess(req, res);
if (!service) return;
const result = await service.resumeGoal({
userId: req.currentUser.id,
goalRunId: req.params.goalRunId,
});
return res.json(result);
} catch (err) {
const status = err?.code === 'GOAL_RUN_NOT_FOUND' || err?.code === 'GOAL_CHECKPOINT_UNAVAILABLE'
? 404
: err?.code === 'GOAL_RUN_NOT_RESUMABLE'
? 409
: 500;
return res.status(status).json({
message: err instanceof Error ? err.message : '续作目标失败',
code: err?.code ?? null,
});
}
});
api.delete('/goals/:goalRunId', async (req, res) => {
try {
const service = ensureGoalRunAccess(req, res);
if (!service) return;
const goal = await service.cancelGoal({
userId: req.currentUser.id,
goalRunId: req.params.goalRunId,
});
return res.json({ goal });
} catch (err) {
const status = err?.code === 'GOAL_RUN_NOT_CANCELLABLE' ? 409 : 500;
return res.status(status).json({
message: err instanceof Error ? err.message : '取消目标失败',
code: err?.code ?? null,
});
}
});
}