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; }; 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 { 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 | null { const byKey = new Map(); 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[] { const found = new Set(); 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}`, }; }