import fs from 'node:fs'; import path from 'node:path'; import { normalizeAgentRunToolMode } from './agent-run-gateway.mjs'; import { AIDER_DEVELOPMENT_SKILL_NAME, buildChatSkillPrompt, extractAiderDevelopmentTask, isPageDataIntent, } from './chat-skills.mjs'; import { createSessionAccess } from './session-broker.mjs'; import { extractRunFromStreamEvent, formatRunStreamSseChunk, isRunStreamReplayEnabled, isTerminalRunStatus, parseRunStreamLastEventId, shouldEmitRunUpdateForStreamEvent, } from './agent-run-stream.mjs'; import { wrapRunStreamPayload, writeSseErrorAndEnd } from './sse-event-taxonomy.mjs'; function envFlag(value) { return ['1', 'true', 'yes', 'on'].includes(String(value ?? '').trim().toLowerCase()); } function parseUserIdSet(value) { return new Set( String(value ?? '') .split(',') .map((item) => item.trim()) .filter(Boolean), ); } function parseTaskTypeSet(value) { return new Set( String(value ?? '') .split(',') .map((item) => item.trim().toLowerCase()) .filter(Boolean), ); } function hasExpectedFileValidation(userMessage) { const metadata = userMessage?.metadata; const runMetadata = metadata?.memindRun ?? metadata?.agentRun ?? {}; const validation = runMetadata.validation ?? metadata?.toolGatewayValidation; if (!validation || typeof validation !== 'object' || Array.isArray(validation)) return false; const expectedFile = validation.expectedFile ?? validation.expectedPath; if (typeof expectedFile === 'string' && expectedFile.trim()) return true; if (expectedFile && typeof expectedFile === 'object' && !Array.isArray(expectedFile)) { const filePath = String(expectedFile.path ?? expectedFile.file ?? expectedFile.relativePath ?? '').trim(); if (filePath) return true; } if (!Array.isArray(validation.expectedFiles)) return false; return validation.expectedFiles.some((item) => { if (typeof item === 'string') return Boolean(item.trim()); if (!item || typeof item !== 'object' || Array.isArray(item)) return false; return Boolean(String(item.path ?? item.file ?? item.relativePath ?? '').trim()); }); } function selectedChatSkill(userMessage) { const metadata = userMessage?.metadata; const runMetadata = metadata?.memindRun ?? metadata?.agentRun ?? {}; return String(runMetadata.selectedChatSkill ?? '').trim(); } function rewriteAiderPageDataInstruction(userMessage, taskText) { const aiderPrompt = buildChatSkillPrompt( AIDER_DEVELOPMENT_SKILL_NAME, AIDER_DEVELOPMENT_SKILL_NAME, ); const pageDataPrompt = buildChatSkillPrompt('page-data-collect', 'page-data-collect'); const compositePrompt = [ pageDataPrompt, taskText, '\n\n[强制 Aider 审查]', '先由当前 Agent 使用 private_data_* 和 Page Data 工具完成建表、dataset、页面与绑定。', '完成后平台会强制调用 Aider 审查当前工作区产物;禁止省略该审查或声称 Aider 已执行。', ].join(''); const content = Array.isArray(userMessage?.content) ? userMessage.content .filter((item) => ( item?.type !== 'text' || !String(item.text ?? '').trim().startsWith('[Memind code-run validation]') )) .map((item) => { if (item?.type !== 'text') return item; const text = String(item.text ?? ''); return { ...item, text: text.includes(aiderPrompt) ? text.replace(`${aiderPrompt}${taskText}`, compositePrompt) : text, }; }) : userMessage?.content; return { ...userMessage, content }; } export function enforceSelectedSkillRuntime(userMessage, { rawToolMode = 'chat', taskType = null, } = {}) { if (selectedChatSkill(userMessage) !== AIDER_DEVELOPMENT_SKILL_NAME) { return { userMessage, rawToolMode, taskType, requiredExecutor: null }; } const message = userMessage && typeof userMessage === 'object' && !Array.isArray(userMessage) ? { ...userMessage } : { value: userMessage }; const metadata = message.metadata && typeof message.metadata === 'object' && !Array.isArray(message.metadata) ? { ...message.metadata } : {}; const runMetadata = metadata.memindRun && typeof metadata.memindRun === 'object' && !Array.isArray(metadata.memindRun) ? { ...metadata.memindRun } : {}; const taskText = extractAiderDevelopmentTask(userMessage); const requiresPageDataBuild = isPageDataIntent(taskText); metadata.memindRun = { ...runMetadata, selectedChatSkill: AIDER_DEVELOPMENT_SKILL_NAME, ...(requiresPageDataBuild ? { reviewExecutor: 'aider', pageDataAiderWorkflow: true } : { executor: 'aider' }), }; if (requiresPageDataBuild) delete metadata.memindRun.executor; return { userMessage: requiresPageDataBuild ? rewriteAiderPageDataInstruction({ ...message, metadata }, taskText) : { ...message, metadata }, rawToolMode: requiresPageDataBuild ? 'chat' : 'code', taskType: requiresPageDataBuild ? null : 'h5_chat_code_task', requiredExecutor: requiresPageDataBuild ? null : 'aider', requiredReviewExecutor: requiresPageDataBuild ? 'aider' : null, }; } export function createPostAgentRunsHandler({ userAuth, sessionAccess = null, agentRunGateway, mindSpaceAssetAgent = null, codeRunPolicyService = null, codeRunsEnabled = envFlag(process.env.MEMIND_AGENT_CODE_RUNS_ENABLED), codeRunUserIds = parseUserIdSet(process.env.MEMIND_AGENT_CODE_RUNS_USER_IDS), codeRunTaskTypes = parseTaskTypeSet(process.env.MEMIND_AGENT_CODE_RUN_TASK_TYPES), requireCodeRunValidation = envFlag(process.env.MEMIND_AGENT_CODE_RUNS_REQUIRE_VALIDATION), }) { const sessionStore = sessionAccess ?? createSessionAccess({ userAuth, enabled: false }); async function resolveCodeRunPolicy(userId) { if (codeRunPolicyService?.getEffectivePolicy) { return codeRunPolicyService.getEffectivePolicy(userId); } return { source: 'env', enabled: codeRunsEnabled, clientEnabled: codeRunsEnabled, generalAutodetect: false, pageDataDevAutodetect: false, requireValidation: requireCodeRunValidation, userAllowlist: [...codeRunUserIds], taskTypeAllowlist: [...codeRunTaskTypes], userAllowed: !codeRunUserIds.size || codeRunUserIds.has(String(userId ?? '').trim()), }; } return async function postAgentRuns(request, response) { try { const releaseDrainFile = process.env.MEMIND_RELEASE_DRAIN_FILE || path.join(process.cwd(), '.release-drain'); if (fs.existsSync(releaseDrainFile)) { response.status(503).json({ message: '系统正在进行发布排水,请稍后重试', code: 'RELEASE_DRAIN_ACTIVE', }); return; } const sessionId = String(request.body?.session_id ?? '').trim() || null; const requestId = String(request.body?.request_id ?? '').trim(); let userMessage = request.body?.user_message ?? null; let rawToolMode = request.body?.tool_mode ?? request.body?.toolMode ?? 'chat'; let taskType = String(request.body?.task_type ?? request.body?.taskType ?? '').trim() || null; const forceDeepReasoning = request.body?.force_deep_reasoning === true || request.body?.forceDeepReasoning === true; if (!requestId) { response.status(400).json({ message: '缺少 request_id' }); return; } if (!userMessage) { response.status(400).json({ message: '缺少 user_message' }); return; } const selectedSkillRuntime = enforceSelectedSkillRuntime(userMessage, { rawToolMode, taskType, }); userMessage = selectedSkillRuntime.userMessage; rawToolMode = selectedSkillRuntime.rawToolMode; taskType = selectedSkillRuntime.taskType; if ( (selectedSkillRuntime.requiredExecutor || selectedSkillRuntime.requiredReviewExecutor) && !extractAiderDevelopmentTask(userMessage) ) { response.status(400).json({ message: '请输入需要 Aider 执行的具体开发任务' }); return; } if ( (selectedSkillRuntime.requiredExecutor || selectedSkillRuntime.requiredReviewExecutor) && userAuth?.getUserSkills ) { const skillState = await userAuth.getUserSkills(request.currentUser.id); if (!skillState?.skills?.[AIDER_DEVELOPMENT_SKILL_NAME]) { response.status(403).json({ message: '当前用户未授权 Aider 开发技能' }); return; } } let toolMode = 'chat'; try { toolMode = normalizeAgentRunToolMode(rawToolMode); } catch (err) { response.status(400).json({ message: err instanceof Error ? err.message : '不支持的 tool_mode', }); return; } if (toolMode === 'code' || selectedSkillRuntime.requiredReviewExecutor) { const codeRunPolicy = await resolveCodeRunPolicy(request.currentUser.id); const policyTaskType = selectedSkillRuntime.requiredReviewExecutor ? 'h5_chat_code_task' : taskType; if (!codeRunPolicy.enabled) { response.status(403).json({ message: '代码任务灰度未开启' }); return; } if (!codeRunPolicy.userAllowed) { response.status(403).json({ message: '当前用户未开启代码任务灰度' }); return; } const taskTypeAllowlist = codeRunPolicy.taskTypeAllowlist ?? []; if ( taskTypeAllowlist.length > 0 && ( !policyTaskType || !taskTypeAllowlist .map((item) => String(item).toLowerCase()) .includes(policyTaskType.toLowerCase()) ) ) { response.status(403).json({ message: '当前代码任务类型未开启灰度' }); return; } if (codeRunPolicy.requireValidation && !hasExpectedFileValidation(userMessage)) { response.status(400).json({ message: '代码任务必须声明产物校验规则' }); return; } } if (sessionId) { const owns = await sessionStore.validateOwnership(request.currentUser.id, sessionId); if (!owns) { response.status(403).json({ message: '无权访问该会话' }); return; } } const selectedAssetIds = [ ...new Set( (Array.isArray(request.body?.selected_asset_ids) ? request.body.selected_asset_ids : Array.isArray(request.body?.selectedAssetIds) ? request.body.selectedAssetIds : [] ) .map((item) => String(item ?? '').trim()) .filter(Boolean), ), ]; if ( selectedAssetIds.length > 0 && mindSpaceAssetAgent?.materializeAssetsForSession ) { try { await mindSpaceAssetAgent.materializeAssetsForSession({ sessionId, userId: request.currentUser.id, assetIds: selectedAssetIds, }); } catch (err) { response.status(400).json({ message: err instanceof Error ? err.message : '勾选资料落盘失败', code: err?.code ?? 'asset_materialize_failed', }); return; } } const run = await agentRunGateway.createRun(request.currentUser.id, { sessionId, requestId, userMessage, toolMode, taskType, ...(forceDeepReasoning ? { forceDeepReasoning: true } : {}), }); response.status(202).json({ run }); } catch (err) { if (err?.code === 'SESSION_RUN_CONFLICT') { response.status(409).json({ message: err.message, code: 'SESSION_RUN_CONFLICT', }); return; } response.status(500).json({ message: err instanceof Error ? err.message : '创建任务失败', }); } }; } export function createGetAgentRunHandler({ agentRunGateway }) { return async function getAgentRun(request, response) { try { const run = await agentRunGateway.getRunForUser( request.currentUser.id, request.params.runId, ); if (!run) { response.status(404).json({ message: '任务不存在' }); return; } if (run.status !== 'succeeded' && run.status !== 'failed') { agentRunGateway.dispatchRun(run.id); } response.json({ run }); } catch (err) { response.status(500).json({ message: err instanceof Error ? err.message : '读取任务失败', }); } }; } function createRunStreamWriter(response, { wrapPayload = true } = {}) { let lastSentEventId = null; let lastRunPayload = null; const writeChunk = (eventId, eventName, data) => { const payload = wrapPayload ? wrapRunStreamPayload(eventName, data) : data; response.write(formatRunStreamSseChunk({ id: eventId, event: eventName, data: payload })); if (eventId) lastSentEventId = eventId; }; const emitRunIfChanged = (run, eventId = null) => { const nextPayload = JSON.stringify(run); if (nextPayload === lastRunPayload) return false; lastRunPayload = nextPayload; writeChunk(eventId, 'run', { run }); return true; }; return { getLastSentEventId: () => lastSentEventId, setLastSentEventId: (value) => { lastSentEventId = value; }, emitRunIfChanged, writeError: (message) => { writeChunk(null, 'error', { message }); }, }; } export function createAgentRunEventsHandler({ agentRunGateway, pollIntervalMs = 1000, keepaliveIntervalMs = 20000, replayEnabled = isRunStreamReplayEnabled(), }) { return async function getAgentRunEvents(request, response) { const userId = request.currentUser.id; const runId = request.params.runId; let closed = false; let pollTimer = null; let keepaliveTimer = null; const initialLastEventId = parseRunStreamLastEventId(request.get?.('last-event-id')); const cleanup = () => { closed = true; if (pollTimer) clearInterval(pollTimer); if (keepaliveTimer) clearInterval(keepaliveTimer); pollTimer = null; keepaliveTimer = null; }; const firstRun = await agentRunGateway.getRunForUser(userId, runId); if (!firstRun) { response.status(404).json({ message: '任务不存在' }); return; } response.status(200); response.setHeader('Content-Type', 'text/event-stream'); response.setHeader('Cache-Control', 'no-cache'); response.setHeader('Connection', 'keep-alive'); request.on('close', cleanup); keepaliveTimer = setInterval(() => { if (!closed) response.write(': keepalive\n\n'); }, keepaliveIntervalMs); const stream = createRunStreamWriter(response); const publishLegacy = async (prefetchedRun = null) => { if (closed) return; try { const run = prefetchedRun ?? await agentRunGateway.getRunForUser(userId, runId); if (!run) { stream.writeError('任务不存在'); cleanup(); response.end(); return; } stream.emitRunIfChanged(run); if (!isTerminalRunStatus(run.status)) { agentRunGateway.dispatchRun(run.id); return; } cleanup(); response.end(); } catch (err) { if (response.headersSent) { writeSseErrorAndEnd(response, err instanceof Error ? err.message : '读取任务失败'); } else { stream.writeError(err instanceof Error ? err.message : '读取任务失败'); response.end(); } cleanup(); } }; const publishReplay = async () => { if (closed) return; try { const cursor = stream.getLastSentEventId() ?? initialLastEventId; const batch = await agentRunGateway.listRunEventsForUser(userId, runId, { afterEventId: cursor, }); if (!batch?.run) { stream.writeError('任务不存在'); cleanup(); response.end(); return; } let latestRun = batch.run; for (const event of batch.events) { if (closed) return; const snapshotRun = extractRunFromStreamEvent(event); if (snapshotRun) { latestRun = snapshotRun; stream.emitRunIfChanged(snapshotRun, event.id); continue; } if (shouldEmitRunUpdateForStreamEvent(event.eventType)) { latestRun = await agentRunGateway.getRunForUser(userId, runId) ?? latestRun; stream.emitRunIfChanged(latestRun, event.id); continue; } response.write(formatRunStreamSseChunk({ id: event.id, event: 'run_event', data: wrapRunStreamPayload('run_event', { eventId: event.id, eventType: event.eventType, data: event.data, createdAt: event.createdAt, }), })); stream.setLastSentEventId(event.id); } if (batch.cursorMiss && !cursor) { stream.emitRunIfChanged(batch.run); } else if (batch.cursorMiss && cursor) { latestRun = await agentRunGateway.getRunForUser(userId, runId) ?? latestRun; stream.emitRunIfChanged(latestRun); } else { stream.emitRunIfChanged(latestRun); } if (!isTerminalRunStatus(latestRun.status)) { agentRunGateway.dispatchRun(runId); return; } cleanup(); response.end(); } catch (err) { if (response.headersSent) { writeSseErrorAndEnd(response, err instanceof Error ? err.message : '读取任务失败'); } else { stream.writeError(err instanceof Error ? err.message : '读取任务失败'); response.end(); } cleanup(); } }; const publish = replayEnabled && agentRunGateway.listRunEventsForUser ? publishReplay : publishLegacy; pollTimer = setInterval(() => { void publish(); }, pollIntervalMs); if (replayEnabled && agentRunGateway.listRunEventsForUser) { await publishReplay(); } else { await publishLegacy(firstRun); } }; }