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, }); } }); }