2026-07-02 12:31:29 +08:00
|
|
|
import type { MindSpaceChatContext } from '../types';
|
|
|
|
|
|
2026-07-02 07:19:26 +08:00
|
|
|
export type AgentRunCreateOptions = {
|
|
|
|
|
toolMode?: 'chat' | 'code';
|
|
|
|
|
taskType?: string | null;
|
2026-07-04 14:57:13 +08:00
|
|
|
forceDeepReasoning?: boolean;
|
2026-07-02 11:52:40 +08:00
|
|
|
validation?: AgentRunValidation | null;
|
|
|
|
|
validationInstruction?: string | null;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export type AgentRunValidationFile = {
|
|
|
|
|
path: string;
|
|
|
|
|
contains?: string;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export type AgentRunValidation = {
|
|
|
|
|
expectedFile?: AgentRunValidationFile | string;
|
|
|
|
|
expectedFiles?: Array<AgentRunValidationFile | string>;
|
2026-07-02 07:19:26 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
);
|
|
|
|
|
|
2026-07-02 08:42:44 +08:00
|
|
|
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));
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-02 07:19:26 +08:00
|
|
|
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}(修改|修复|重构|调试|实现|新增|编写|更新)/,
|
|
|
|
|
];
|
|
|
|
|
|
2026-07-04 14:57:13 +08:00
|
|
|
const DEEP_REASONING_TASK_PATTERNS = [
|
2026-07-04 14:48:45 +08:00
|
|
|
/\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,
|
|
|
|
|
];
|
|
|
|
|
|
2026-07-02 11:52:40 +08:00
|
|
|
function sanitizeRequestIdForPath(requestId: string): string {
|
|
|
|
|
const normalized = String(requestId ?? '').trim().replace(/[^a-zA-Z0-9._-]/g, '-');
|
|
|
|
|
return normalized || 'unknown-request';
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-02 12:31:29 +08:00
|
|
|
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'),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-02 11:52:40 +08:00
|
|
|
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'),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-02 07:19:26 +08:00
|
|
|
export function resolveAgentRunOptions(
|
|
|
|
|
text: string,
|
|
|
|
|
{
|
|
|
|
|
taskType = 'code_task',
|
|
|
|
|
forceCode = false,
|
|
|
|
|
allowAutodetect = agentCodeRunsAutodetectEnabled,
|
2026-07-02 08:42:44 +08:00
|
|
|
userId = null,
|
2026-07-02 11:52:40 +08:00
|
|
|
requestId = null,
|
2026-07-02 12:31:29 +08:00
|
|
|
mindspaceContext = null,
|
|
|
|
|
pageEdit = null,
|
2026-07-02 07:19:26 +08:00
|
|
|
}: {
|
|
|
|
|
taskType?: string;
|
|
|
|
|
forceCode?: boolean;
|
|
|
|
|
allowAutodetect?: boolean;
|
2026-07-02 08:42:44 +08:00
|
|
|
userId?: string | null;
|
2026-07-02 11:52:40 +08:00
|
|
|
requestId?: string | null;
|
2026-07-02 12:31:29 +08:00
|
|
|
mindspaceContext?: MindSpaceChatContext | null;
|
|
|
|
|
pageEdit?: { pageId?: string | null; pageTitle?: string | null } | null;
|
2026-07-02 07:19:26 +08:00
|
|
|
} = {},
|
|
|
|
|
): AgentRunCreateOptions {
|
|
|
|
|
const normalizedText = String(text ?? '').trim();
|
2026-07-04 14:57:13 +08:00
|
|
|
const shouldUseDeepReasoning =
|
|
|
|
|
forceCode || DEEP_REASONING_TASK_PATTERNS.some((pattern) => pattern.test(normalizedText));
|
2026-07-04 14:48:45 +08:00
|
|
|
|
|
|
|
|
if (!agentCodeRunsEnabledForUser(userId)) {
|
2026-07-04 14:57:13 +08:00
|
|
|
return shouldUseDeepReasoning ? { forceDeepReasoning: true, taskType } : {};
|
2026-07-04 14:48:45 +08:00
|
|
|
}
|
|
|
|
|
|
2026-07-02 07:19:26 +08:00
|
|
|
const shouldUseCode = forceCode || (allowAutodetect && CODE_TASK_PATTERNS.some((pattern) => pattern.test(normalizedText)));
|
2026-07-04 14:48:45 +08:00
|
|
|
if (!shouldUseCode) {
|
2026-07-04 14:57:13 +08:00
|
|
|
return shouldUseDeepReasoning ? { forceDeepReasoning: true, taskType } : {};
|
2026-07-04 14:48:45 +08:00
|
|
|
}
|
2026-07-02 12:31:29 +08:00
|
|
|
const normalizedRequestId = requestId ?? crypto.randomUUID();
|
|
|
|
|
const receipt = buildAgentRunValidationReceipt(normalizedRequestId);
|
|
|
|
|
const taskValidation = buildAgentRunTaskValidation({
|
|
|
|
|
requestId: normalizedRequestId,
|
|
|
|
|
taskType,
|
|
|
|
|
text: normalizedText,
|
|
|
|
|
mindspaceContext,
|
|
|
|
|
pageEdit,
|
|
|
|
|
});
|
2026-07-02 07:19:26 +08:00
|
|
|
return {
|
|
|
|
|
toolMode: 'code',
|
|
|
|
|
taskType,
|
2026-07-04 14:57:13 +08:00
|
|
|
forceDeepReasoning: true,
|
2026-07-02 12:31:29 +08:00
|
|
|
validation: mergeAgentRunValidationFiles(receipt.validation, taskValidation.validation),
|
|
|
|
|
validationInstruction: `${receipt.instruction}${taskValidation.instruction}`,
|
2026-07-02 07:19:26 +08:00
|
|
|
};
|
|
|
|
|
}
|