Files
memind/agent-run-routes.mjs
john fde6503bdf
Memind CI / Test, build, and release guards (push) Has been cancelled
feat(mindspace): add SEO/GEO delivery, page template catalog, and admin hooks
Enable optional SEO/GEO injection and discovery routes for confirmed public pages while keeping private pages noindex. Add premium page template skills, portal catalog API, template shop UI, and Baidu push gated by memind_adm config.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-10 08:06:12 +08:00

597 lines
21 KiB
JavaScript

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 {
buildPageTemplateMandatoryInstruction,
isPageTemplateSkill,
} from './page-template-enforcement.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';
import { resolveGoalBindingForAgentRun } from './goal-run-resolve.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 enforcePageTemplateRuntime(userMessage, { h5Root = process.cwd() } = {}) {
const skillName = selectedChatSkill(userMessage);
if (!isPageTemplateSkill(skillName)) {
return { userMessage, pageTemplateSkill: 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 }
: {};
metadata.memindRun = {
...runMetadata,
selectedChatSkill: skillName,
pageTemplateSkill: skillName,
pageTemplateStrict: true,
};
const mandatory = buildPageTemplateMandatoryInstruction(skillName, h5Root);
const content = Array.isArray(message.content)
? message.content.map((item) => {
if (item?.type !== 'text') return item;
const text = String(item.text ?? '').trim();
if (!text || text.startsWith('【强制页面模板:')) return item;
return {
...item,
text: `${mandatory}\n\n${text}`,
};
})
: message.content;
return {
userMessage: { ...message, metadata, content },
pageTemplateSkill: skillName,
};
}
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,
goalRunService = null,
chatIntentRouter = null,
templateCatalogService = null,
env = process.env,
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 pageTemplateRuntime = enforcePageTemplateRuntime(userMessage, { h5Root: process.cwd() });
userMessage = pageTemplateRuntime.userMessage;
if (pageTemplateRuntime.pageTemplateSkill && templateCatalogService?.recordTemplateUsage) {
void templateCatalogService
.recordTemplateUsage(request.currentUser.id, pageTemplateRuntime.pageTemplateSkill)
.catch(() => {});
}
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;
}
}
let goalBinding = null;
if (goalRunService) {
goalBinding = await resolveGoalBindingForAgentRun({
goalRunService,
chatIntentRouter,
userId: request.currentUser.id,
userMessage,
sessionId,
body: request.body ?? {},
env,
});
}
const run = await agentRunGateway.createRun(request.currentUser.id, {
sessionId,
requestId,
userMessage,
toolMode,
taskType,
...(forceDeepReasoning ? { forceDeepReasoning: true } : {}),
...(goalBinding?.goalRunId ? { goalRunId: goalBinding.goalRunId } : {}),
...(goalBinding?.goalCheckpointId ? { goalCheckpointId: goalBinding.goalCheckpointId } : {}),
});
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;
}
if (err?.code === 'GOAL_RUN_NOT_FOUND') {
response.status(404).json({
message: err.message,
code: 'GOAL_RUN_NOT_FOUND',
});
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);
}
};
}