666db0b939
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>
240 lines
7.3 KiB
JavaScript
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,
|
|
});
|
|
}
|
|
});
|
|
}
|