Files
memind/cursor-page-routing.mjs
T
john 416bf7a29a
Memind CI / Test, build, and release guards (push) Failing after 4m22s
Isolate Cursor executor to admin-configured whitelist channel for H5 and WeChat.
Non-allowlisted users stay on the existing Goose/DeepSeek path; only memindadm whitelist users enter the TKMind Cursor channel on selected intents and channels.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-28 15:27:10 +08:00

259 lines
8.4 KiB
JavaScript

import {
AIDER_DEVELOPMENT_SKILL_NAME,
EXCEL_ANALYST_SKILL_NAME,
PAGE_DATA_COLLECT_SKILL_NAME,
extractAiderDevelopmentTask,
isExcelAnalysisIntent,
isGenericPageGenerationRequest,
isPageDataDevIntent,
isPageDataIntent,
isPageGenerationIntent,
} from './chat-skills.mjs';
import {
cursorAgentTasksDefaultEnabled,
cursorExecutorEnabled,
cursorPageTasksDefaultEnabled,
resolvePreferredCodeExecutor,
} from './cursor-agent-launch.mjs';
const PUBLISH_SKILL_NAME = 'static-page-publish';
const PAGE_CURSOR_TASK_TYPE = 'h5_chat_code_task';
const CURSOR_TASK_KIND = {
PAGE_GENERATION: 'page_generation',
PAGE_DATA: 'page_data',
EXCEL_ANALYSIS: 'excel_analysis',
};
function selectedChatSkill(userMessage) {
const metadata = userMessage?.metadata;
const runMetadata = metadata?.memindRun ?? metadata?.agentRun ?? {};
return String(runMetadata.selectedChatSkill ?? '').trim();
}
export function resolveCursorTaskKind(taskText, skill = '') {
const normalized = String(taskText ?? '').trim();
if (!normalized || isPageDataDevIntent(normalized)) return null;
const selectedSkill = String(skill ?? '').trim();
if (
selectedSkill === PAGE_DATA_COLLECT_SKILL_NAME
|| isPageDataIntent(normalized)
) {
return CURSOR_TASK_KIND.PAGE_DATA;
}
if (
selectedSkill === EXCEL_ANALYST_SKILL_NAME
|| isExcelAnalysisIntent(normalized)
) {
return CURSOR_TASK_KIND.EXCEL_ANALYSIS;
}
if (
selectedSkill === PUBLISH_SKILL_NAME
|| isPageGenerationIntent(normalized)
) {
if (isGenericPageGenerationRequest(normalized)) return null;
return CURSOR_TASK_KIND.PAGE_GENERATION;
}
return null;
}
export function isPageCursorDefaultCandidate(userMessage, {
rawToolMode = 'chat',
env = process.env,
channelEligible = false,
} = {}) {
if (!cursorExecutorEnabled(env)) return false;
if (!channelEligible) return false;
if (String(rawToolMode ?? '').trim().toLowerCase() === 'code') return false;
const skill = selectedChatSkill(userMessage);
if (skill === AIDER_DEVELOPMENT_SKILL_NAME) return false;
const runMetadata = userMessage?.metadata?.memindRun ?? userMessage?.metadata?.agentRun ?? {};
if (runMetadata.pageTemplateSkill || runMetadata.pageTemplateStrict) return false;
const taskText = extractAiderDevelopmentTask(userMessage);
if (!taskText) return false;
return resolveCursorTaskKind(taskText, skill) !== null;
}
function buildCursorPageInstruction(taskText) {
return [
taskText,
'',
'[Memind page task via TKMind 智趣 executor]',
'在当前用户 MindSpace 工作区生成或更新 public/*.html 页面。',
'完成后确保页面可通过 /MindSpace/<userId>/public/<file>.html 访问。',
'不要只回复文字,必须实际落盘 HTML 文件。',
].join('\n');
}
function buildCursorPageDataInstruction(taskText) {
return [
taskText,
'',
'[Memind Page Data survey task via TKMind 智趣 executor]',
'在当前用户 MindSpace 工作区完成可提交、可持久化的问卷/数据收集与结果分析页面。',
'必须先阅读 docs/page-data-api-usage.md 与 docs/examples/page-data-survey-test.md。',
'需要:1) 创建/更新 public/*.html 问卷页(含 MindSpacePageData.createClient);',
'2) 按需创建后台分析页;3) 注册 dataset 与 page policy(可参考 scripts/setup-page-data-survey-demo.mjs);',
'4) 数据必须持久化到 PG(通过 Page Data API),不要旁路自建 API。',
'完成后确保页面可通过 /MindSpace/<userId>/public/<file>.html 访问。',
'不要只回复文字,必须实际落盘 HTML 与相关配置文件。',
].join('\n');
}
function buildCursorExcelInstruction(taskText) {
return [
taskText,
'',
'[Memind Excel analysis task via TKMind 智趣 executor]',
'在当前用户 MindSpace 工作区分析用户上传的 Excel/CSV 等表格文件。',
'先在工作区 attachments/、uploads/ 或用户消息提及的路径定位附件文件,读取并分析数据。',
'输出分析结论,并生成可访问的结果页面 public/*.html 或分析报告文件。',
'不要只回复文字,必须实际读取文件并产出可交付结果。',
].join('\n');
}
function buildCursorTaskInstruction(taskText, taskKind) {
switch (taskKind) {
case CURSOR_TASK_KIND.PAGE_DATA:
return buildCursorPageDataInstruction(taskText);
case CURSOR_TASK_KIND.EXCEL_ANALYSIS:
return buildCursorExcelInstruction(taskText);
case CURSOR_TASK_KIND.PAGE_GENERATION:
default:
return buildCursorPageInstruction(taskText);
}
}
function resolveCursorTaskSkill(taskKind, runMetadataSkill = '') {
const existing = String(runMetadataSkill ?? '').trim();
if (existing && existing !== AIDER_DEVELOPMENT_SKILL_NAME) return existing;
switch (taskKind) {
case CURSOR_TASK_KIND.PAGE_DATA:
return PAGE_DATA_COLLECT_SKILL_NAME;
case CURSOR_TASK_KIND.EXCEL_ANALYSIS:
return EXCEL_ANALYST_SKILL_NAME;
case CURSOR_TASK_KIND.PAGE_GENERATION:
default:
return PUBLISH_SKILL_NAME;
}
}
function resolveCursorSuggestedDelivery(taskKind) {
switch (taskKind) {
case CURSOR_TASK_KIND.PAGE_DATA:
return 'mindspace_page_data';
case CURSOR_TASK_KIND.EXCEL_ANALYSIS:
return 'mindspace_analysis_html';
case CURSOR_TASK_KIND.PAGE_GENERATION:
default:
return 'mindspace_public_html';
}
}
function rewriteUserMessageForCursorTask(userMessage, taskText, taskKind) {
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 instruction = buildCursorTaskInstruction(taskText, taskKind);
metadata.displayText = taskText;
metadata.userVisible = true;
const content = Array.isArray(message.content)
? message.content.map((item) => (
item?.type === 'text'
? { ...item, text: instruction }
: item
))
: [{ type: 'text', text: instruction }];
return { ...message, metadata, content };
}
export function enforcePageGenerationCursorRuntime(userMessage, {
rawToolMode = 'chat',
taskType = null,
forceDeepReasoning = false,
env = process.env,
channelEligible = false,
} = {}) {
if (!isPageCursorDefaultCandidate(userMessage, { rawToolMode, env, channelEligible })) {
return {
userMessage,
rawToolMode,
taskType,
forceDeepReasoning,
requiredExecutor: null,
};
}
const taskText = extractAiderDevelopmentTask(userMessage);
const skill = selectedChatSkill(userMessage);
const taskKind = resolveCursorTaskKind(taskText, skill);
if (!taskKind) {
return {
userMessage,
rawToolMode,
taskType,
forceDeepReasoning,
requiredExecutor: null,
};
}
const codeExecutor = resolvePreferredCodeExecutor(env);
const message = rewriteUserMessageForCursorTask(userMessage, taskText, taskKind);
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,
executor: codeExecutor,
pageCursorDefault: true,
cursorTaskKind: taskKind,
cursorChannel: true,
channel: 'h5',
suggestedDelivery: resolveCursorSuggestedDelivery(taskKind),
selectedChatSkill: resolveCursorTaskSkill(taskKind, runMetadata.selectedChatSkill),
};
return {
userMessage: { ...message, metadata },
rawToolMode: 'code',
taskType: PAGE_CURSOR_TASK_TYPE,
forceDeepReasoning: true,
requiredExecutor: codeExecutor,
};
}
export function applyCursorFirstAgentExecution(userMessage, runOptions = {}, {
routingDecision = null,
env = process.env,
} = {}) {
// Cursor 仅服务于显式页面/代码任务(enforcePageGenerationCursorRuntime)。
// 普通 Agent 编排走 Goose/DeepSeek,不因 MEMIND_CURSOR_AGENT_TASKS_DEFAULT 全量切 Cursor。
void userMessage;
void runOptions;
void routingDecision;
void env;
return { userMessage, runOptions, cursorFirst: false };
}
export function __cursorPageRoutingTestExports() {
return {
PAGE_CURSOR_TASK_TYPE,
CURSOR_TASK_KIND,
cursorPageTasksDefaultEnabled,
cursorAgentTasksDefaultEnabled,
};
}