7f8d692d16
Fix DEV logout cookie clearing, materialize selected MindSpace OA assets before agent runs, and recover zombie runs from synced workspace pages. Add client run wait timeout, harness retry limits, page-edit asset forwarding, and logout/john2 scenario tests. Co-authored-by: Cursor <cursoragent@cursor.com>
268 lines
9.5 KiB
TypeScript
268 lines
9.5 KiB
TypeScript
import type { MindSpaceChatContext } from '../types';
|
|
|
|
export type AgentRunCreateOptions = {
|
|
toolMode?: 'chat' | 'code';
|
|
taskType?: string | null;
|
|
forceDeepReasoning?: boolean;
|
|
validation?: AgentRunValidation | null;
|
|
validationInstruction?: string | null;
|
|
selectedAssetIds?: string[];
|
|
};
|
|
|
|
export type AgentRunValidationFile = {
|
|
path: string;
|
|
contains?: string;
|
|
};
|
|
|
|
export type AgentRunValidation = {
|
|
expectedFile?: AgentRunValidationFile | string;
|
|
expectedFiles?: Array<AgentRunValidationFile | string>;
|
|
};
|
|
|
|
function envFlag(value: unknown): boolean {
|
|
const normalized = String(value ?? '').trim().toLowerCase();
|
|
return ['1', 'true', 'yes', 'on'].includes(normalized);
|
|
}
|
|
|
|
export const agentCodeRunsEnabled = envFlag(import.meta.env.VITE_AGENT_CODE_RUNS_ENABLED);
|
|
export const agentCodeRunsAutodetectEnabled = envFlag(
|
|
import.meta.env.VITE_AGENT_CODE_RUNS_AUTODETECT,
|
|
);
|
|
|
|
function parseUserIdSet(value: unknown): Set<string> {
|
|
return new Set(
|
|
String(value ?? '')
|
|
.split(',')
|
|
.map((item) => item.trim())
|
|
.filter(Boolean),
|
|
);
|
|
}
|
|
|
|
const agentCodeRunUserIds = parseUserIdSet(import.meta.env.VITE_AGENT_CODE_RUNS_USER_IDS);
|
|
|
|
export function agentCodeRunsEnabledForUser(userId?: string | null): boolean {
|
|
if (!agentCodeRunsEnabled) return false;
|
|
if (agentCodeRunUserIds.size === 0) return true;
|
|
return Boolean(userId && agentCodeRunUserIds.has(userId));
|
|
}
|
|
|
|
const CODE_TASK_PATTERNS = [
|
|
/\b(repo|repository|branch|commit|pull request|pr|diff|patch)\b/i,
|
|
/\b(aider|openhands|codex|codebase|workspace)\b/i,
|
|
/\b(refactor|debug|fix bug|implement|add test|unit test)\b/i,
|
|
/(修改|修复|重构|调试|实现|新增|编写|更新).{0,12}(代码|仓库|分支|文件|测试|组件|接口)/,
|
|
/(代码|仓库|项目|文件|组件|接口).{0,12}(修改|修复|重构|调试|实现|新增|编写|更新)/,
|
|
];
|
|
|
|
const DEEP_REASONING_TASK_PATTERNS = [
|
|
/\b(public\/[^\s"'<>]+\.html)\b/i,
|
|
/\b(html|h5|web\s?page|landing\s?page|microsite|docx|word)\b/i,
|
|
/\b(write|create|generate|publish|download|export|save|edit)\b.{0,40}\b(file|page|html|docx|word|asset)\b/i,
|
|
/\b(file|page|html|docx|word|asset)\b.{0,40}\b(write|create|generate|publish|download|export|save|edit)\b/i,
|
|
/(?:生成|创建|制作|做|写|设计|发布|导出|下载|保存|修改|编辑).{0,24}(?:页面|网页|HTML|html|H5|h5|文件|文档|Word|word|docx|下载页|公开页|分享页|落地页|活动页)/u,
|
|
/(?:页面|网页|HTML|html|H5|h5|文件|文档|Word|word|docx|下载页|公开页|分享页|落地页|活动页).{0,24}(?:生成|创建|制作|做|写|设计|发布|导出|下载|保存|修改|编辑)/u,
|
|
/MindSpace\/[^/\s]+\/public\/[^\s"'<>]+\.html/i,
|
|
];
|
|
|
|
function sanitizeRequestIdForPath(requestId: string): string {
|
|
const normalized = String(requestId ?? '').trim().replace(/[^a-zA-Z0-9._-]/g, '-');
|
|
return normalized || 'unknown-request';
|
|
}
|
|
|
|
function normalizeExpectedFile(value: AgentRunValidationFile | string): AgentRunValidationFile | null {
|
|
if (typeof value === 'string') {
|
|
const path = value.trim();
|
|
return path ? { path } : null;
|
|
}
|
|
const path = String(value?.path ?? '').trim();
|
|
if (!path) return null;
|
|
return {
|
|
path,
|
|
...(value.contains == null ? {} : { contains: String(value.contains) }),
|
|
};
|
|
}
|
|
|
|
function mergeAgentRunValidationFiles(
|
|
...items: Array<AgentRunValidation | AgentRunValidationFile | string | null | undefined>
|
|
): AgentRunValidation | null {
|
|
const byKey = new Map<string, AgentRunValidationFile>();
|
|
const addFile = (file: AgentRunValidationFile | string | null | undefined) => {
|
|
if (!file) return;
|
|
const normalized = normalizeExpectedFile(file);
|
|
if (!normalized) return;
|
|
const key = `${normalized.path}\u0000${normalized.contains ?? ''}`;
|
|
byKey.set(key, normalized);
|
|
};
|
|
|
|
for (const item of items) {
|
|
if (!item) continue;
|
|
if (typeof item === 'string' || 'path' in item) {
|
|
addFile(item);
|
|
continue;
|
|
}
|
|
addFile(item.expectedFile);
|
|
for (const file of item.expectedFiles ?? []) addFile(file);
|
|
}
|
|
|
|
const expectedFiles = [...byKey.values()];
|
|
return expectedFiles.length ? { expectedFiles } : null;
|
|
}
|
|
|
|
export function normalizeAgentRunPublicHtmlPath(candidate: string): string | null {
|
|
const decoded = decodeURIComponent(String(candidate ?? '')).replace(/\\/g, '/').trim();
|
|
const publicMatch = decoded.match(/(?:^|\/)(public\/[^"'<>?#\s]+\.html)\b/i);
|
|
const raw = publicMatch?.[1] ?? '';
|
|
if (!raw) return null;
|
|
const parts = raw
|
|
.split('/')
|
|
.map((part) => part.trim())
|
|
.filter(Boolean);
|
|
if (parts.length < 2 || parts[0] !== 'public') return null;
|
|
if (parts.some((part) => part === '.' || part === '..')) return null;
|
|
return parts.join('/');
|
|
}
|
|
|
|
export function extractAgentRunPublicHtmlPaths(...values: Array<string | null | undefined>): string[] {
|
|
const found = new Set<string>();
|
|
const pattern = /(?:^|[\s"'([{<])((?:https?:\/\/[^\s"'<>]+|\/?MindSpace\/[^\s"'<>]+|public\/[^\s"'<>]+)\.html)\b/gi;
|
|
for (const value of values) {
|
|
const text = String(value ?? '');
|
|
let match: RegExpExecArray | null;
|
|
while ((match = pattern.exec(text))) {
|
|
const path = normalizeAgentRunPublicHtmlPath(match[1] ?? '');
|
|
if (path) found.add(path);
|
|
}
|
|
}
|
|
return [...found];
|
|
}
|
|
|
|
export function buildAgentRunTaskValidation({
|
|
requestId,
|
|
taskType,
|
|
text,
|
|
mindspaceContext,
|
|
pageEdit,
|
|
}: {
|
|
requestId: string;
|
|
taskType: string;
|
|
text: string;
|
|
mindspaceContext?: MindSpaceChatContext | null;
|
|
pageEdit?: { pageId?: string | null; pageTitle?: string | null } | null;
|
|
}): { validation: AgentRunValidation | null; instruction: string } {
|
|
const safeRequestId = sanitizeRequestIdForPath(requestId);
|
|
const pageId = String(pageEdit?.pageId ?? mindspaceContext?.page?.id ?? '').trim();
|
|
const pageTitle = String(pageEdit?.pageTitle ?? mindspaceContext?.page?.title ?? '').trim();
|
|
const publicHtmlPaths = extractAgentRunPublicHtmlPaths(
|
|
text,
|
|
mindspaceContext?.page?.publicationUrl ?? null,
|
|
);
|
|
const expectedFiles: AgentRunValidationFile[] = publicHtmlPaths.map((path) => ({ path }));
|
|
const instructions: string[] = [];
|
|
|
|
if (publicHtmlPaths.length > 0) {
|
|
instructions.push(
|
|
'',
|
|
'[Memind task artifact validation]',
|
|
'The task references concrete MindSpace public HTML artifacts.',
|
|
'Before finishing, ensure these workspace-relative files exist and contain the intended result:',
|
|
...publicHtmlPaths.map((path) => `- ${path}`),
|
|
);
|
|
}
|
|
|
|
if (taskType === 'page_edit_code_task' && pageId) {
|
|
const taskReceiptPath = `.memind/agent-runs/${safeRequestId}-page-edit.json`;
|
|
expectedFiles.push({ path: taskReceiptPath, contains: pageId });
|
|
instructions.push(
|
|
'',
|
|
'[Memind page-edit validation]',
|
|
`Before finishing this page-edit task, create or update ${taskReceiptPath}.`,
|
|
'The file must be valid JSON and include:',
|
|
`- requestId: ${requestId}`,
|
|
`- taskType: ${taskType}`,
|
|
`- pageId: ${pageId}`,
|
|
...(pageTitle ? [`- pageTitle: ${pageTitle}`] : []),
|
|
'- a brief summary of the page changes or the reason no change was needed.',
|
|
);
|
|
}
|
|
|
|
return {
|
|
validation: expectedFiles.length ? { expectedFiles } : null,
|
|
instruction: instructions.join('\n'),
|
|
};
|
|
}
|
|
|
|
export function buildAgentRunValidationReceipt(requestId: string): {
|
|
validation: AgentRunValidation;
|
|
instruction: string;
|
|
} {
|
|
const normalizedRequestId = String(requestId ?? '').trim();
|
|
const safeRequestId = sanitizeRequestIdForPath(normalizedRequestId);
|
|
const receiptPath = `.memind/agent-runs/${safeRequestId}.json`;
|
|
const marker = normalizedRequestId || safeRequestId;
|
|
return {
|
|
validation: {
|
|
expectedFile: {
|
|
path: receiptPath,
|
|
contains: marker,
|
|
},
|
|
},
|
|
instruction: [
|
|
'',
|
|
'[Memind code-run validation]',
|
|
`Before finishing, create or update ${receiptPath}.`,
|
|
`The file must be valid JSON and include this requestId value: ${marker}.`,
|
|
'Do not skip this receipt even if the user task itself is complete.',
|
|
].join('\n'),
|
|
};
|
|
}
|
|
|
|
export function resolveAgentRunOptions(
|
|
text: string,
|
|
{
|
|
taskType = 'code_task',
|
|
forceCode = false,
|
|
allowAutodetect = agentCodeRunsAutodetectEnabled,
|
|
userId = null,
|
|
requestId = null,
|
|
mindspaceContext = null,
|
|
pageEdit = null,
|
|
}: {
|
|
taskType?: string;
|
|
forceCode?: boolean;
|
|
allowAutodetect?: boolean;
|
|
userId?: string | null;
|
|
requestId?: string | null;
|
|
mindspaceContext?: MindSpaceChatContext | null;
|
|
pageEdit?: { pageId?: string | null; pageTitle?: string | null } | null;
|
|
} = {},
|
|
): AgentRunCreateOptions {
|
|
const normalizedText = String(text ?? '').trim();
|
|
const shouldUseDeepReasoning =
|
|
forceCode || DEEP_REASONING_TASK_PATTERNS.some((pattern) => pattern.test(normalizedText));
|
|
|
|
if (!agentCodeRunsEnabledForUser(userId)) {
|
|
return shouldUseDeepReasoning ? { forceDeepReasoning: true, taskType } : {};
|
|
}
|
|
|
|
const shouldUseCode = forceCode || (allowAutodetect && CODE_TASK_PATTERNS.some((pattern) => pattern.test(normalizedText)));
|
|
if (!shouldUseCode) {
|
|
return shouldUseDeepReasoning ? { forceDeepReasoning: true, taskType } : {};
|
|
}
|
|
const normalizedRequestId = requestId ?? crypto.randomUUID();
|
|
const receipt = buildAgentRunValidationReceipt(normalizedRequestId);
|
|
const taskValidation = buildAgentRunTaskValidation({
|
|
requestId: normalizedRequestId,
|
|
taskType,
|
|
text: normalizedText,
|
|
mindspaceContext,
|
|
pageEdit,
|
|
});
|
|
return {
|
|
toolMode: 'code',
|
|
taskType,
|
|
forceDeepReasoning: true,
|
|
validation: mergeAgentRunValidationFiles(receipt.validation, taskValidation.validation),
|
|
validationInstruction: `${receipt.instruction}${taskValidation.instruction}`,
|
|
};
|
|
}
|