feat(cursor): wire executor through portal, admin, and wechat routes

Register cursor in LLM executor bindings, route page/code tasks through
cursor runtime selection, and bootstrap wechat cursor policy services.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-08-27 09:33:47 +08:00
parent a29e1ec5c8
commit ad6dcc283f
17 changed files with 343 additions and 16 deletions
+3
View File
@@ -34,6 +34,7 @@ import { createOrchestratorObservabilityService } from './services/orchestrator/
import { createSkillRuntimeAdminConfigService } from './skill-runtime-admin-config.mjs';
import { createSystemDisclosurePolicyService } from './system-disclosure-policy.mjs';
import { createAgentCodeRunAdminConfigService } from './agent-code-run-admin-config.mjs';
import { createWechatCursorExecutorAdminConfigService } from './wechat-cursor-executor-admin-config.mjs';
import { createMindSearchConfigService } from './mindsearch-config.mjs';
import { createWechatScheduleLlmConfigService } from './wechat-schedule-llm-config.mjs';
import { createWechatIntentRouterConfigService } from './wechat-intent-router-config.mjs';
@@ -150,6 +151,7 @@ export async function createAdminServices(env = {}) {
});
await systemDisclosurePolicyService.initialize();
const agentCodeRunPolicyService = createAgentCodeRunAdminConfigService(pool, { env: process.env });
const wechatCursorExecutorPolicyService = createWechatCursorExecutorAdminConfigService(pool);
const wechatScheduleLlmConfigService = createWechatScheduleLlmConfigService(pool);
const wechatIntentRouterConfigService = createWechatIntentRouterConfigService(pool);
const adminSystemTestService = createAdminSystemTestService({
@@ -198,6 +200,7 @@ export async function createAdminServices(env = {}) {
skillRuntimeConfigService,
systemDisclosurePolicyService,
agentCodeRunPolicyService,
wechatCursorExecutorPolicyService,
wechatScheduleLlmConfigService,
wechatIntentRouterConfigService,
adminSystemTestService,
+30
View File
@@ -43,6 +43,7 @@ function plazaRouteError(res, req, error) {
* @param {object|null} deps.plazaOps
* @param {object|null} deps.wechatAdmin
* @param {object|null} deps.wechatIntentRouterConfigService
* @param {object|null} deps.wechatCursorExecutorPolicyService
*/
export function createAdminApi({
jsonBody,
@@ -66,6 +67,7 @@ export function createAdminApi({
plazaOps,
wechatAdmin,
wechatIntentRouterConfigService,
wechatCursorExecutorPolicyService,
subscriptionService,
templateCatalogService,
}) {
@@ -925,6 +927,34 @@ export function createAdminApi({
adminApi.put('/wechat/intent-router/config', requireAdmin, updateWechatIntentRouterConfig);
adminApi.patch('/wechat/intent-router/config', requireAdmin, updateWechatIntentRouterConfig);
adminApi.get('/wechat/cursor-executor/config', requireAdmin, async (_req, res) => {
if (!wechatCursorExecutorPolicyService?.getAdminConfig) {
return res.status(503).json({ message: '微信 Cursor 体验通道未启用' });
}
return res.json(await wechatCursorExecutorPolicyService.getAdminConfig());
});
adminApi.get('/wechat/cursor-executor/runtime', requireAdmin, async (_req, res) => {
if (!wechatCursorExecutorPolicyService?.getRuntimeState) {
return res.status(503).json({ message: '微信 Cursor 体验通道未启用' });
}
return res.json(await wechatCursorExecutorPolicyService.getRuntimeState());
});
const updateWechatCursorExecutorConfig = async (req, res) => {
if (!wechatCursorExecutorPolicyService?.updateAdminConfig) {
return res.status(503).json({ message: '微信 Cursor 体验通道未启用' });
}
const result = await wechatCursorExecutorPolicyService.updateAdminConfig(
req.body?.config ?? req.body ?? {},
{ updatedBy: req.currentUser.id },
);
return res.json(result);
};
adminApi.put('/wechat/cursor-executor/config', requireAdmin, updateWechatCursorExecutorConfig);
adminApi.patch('/wechat/cursor-executor/config', requireAdmin, updateWechatCursorExecutorConfig);
adminApi.get('/llm-providers/catalog', requireAdmin, (_req, res) => {
if (!llmProviderService) return res.status(503).json({ message: '未启用 LLM 配置' });
res.json({ catalog: llmProviderService.catalog });
+1
View File
@@ -87,6 +87,7 @@ const CONSOLES = {
skillRuntimeConfigService: services.skillRuntimeConfigService,
systemDisclosurePolicyService: services.systemDisclosurePolicyService,
agentCodeRunPolicyService: services.agentCodeRunPolicyService,
wechatCursorExecutorPolicyService: services.wechatCursorExecutorPolicyService,
adminSystemTestService: services.adminSystemTestService,
plazaPosts: services.plazaPosts,
plazaOps: services.plazaOps,
+39 -9
View File
@@ -23,6 +23,8 @@ import {
} from './agent-run-stream.mjs';
import { wrapRunStreamPayload, writeSseErrorAndEnd } from './sse-event-taxonomy.mjs';
import { resolveGoalBindingForAgentRun } from './goal-run-resolve.mjs';
import { enforcePageGenerationCursorRuntime } from './cursor-page-routing.mjs';
import { resolvePreferredCodeExecutor } from './cursor-agent-launch.mjs';
function envFlag(value) {
return ['1', 'true', 'yes', 'on'].includes(String(value ?? '').trim().toLowerCase());
@@ -160,12 +162,13 @@ export function enforceSelectedSkillRuntime(userMessage, {
: {};
const taskText = extractAiderDevelopmentTask(userMessage);
const requiresPageDataBuild = isPageDataIntent(taskText);
const codeExecutor = resolvePreferredCodeExecutor();
metadata.memindRun = {
...runMetadata,
selectedChatSkill: AIDER_DEVELOPMENT_SKILL_NAME,
...(requiresPageDataBuild
? { reviewExecutor: 'aider', pageDataAiderWorkflow: true }
: { executor: 'aider' }),
: { executor: codeExecutor }),
};
if (requiresPageDataBuild) delete metadata.memindRun.executor;
return {
@@ -174,7 +177,7 @@ export function enforceSelectedSkillRuntime(userMessage, {
: { ...message, metadata },
rawToolMode: requiresPageDataBuild ? 'chat' : 'code',
taskType: requiresPageDataBuild ? null : 'h5_chat_code_task',
requiredExecutor: requiresPageDataBuild ? null : 'aider',
requiredExecutor: requiresPageDataBuild ? null : codeExecutor,
requiredReviewExecutor: requiresPageDataBuild ? 'aider' : null,
};
}
@@ -253,15 +256,35 @@ export function createPostAgentRunsHandler({
userMessage = selectedSkillRuntime.userMessage;
rawToolMode = selectedSkillRuntime.rawToolMode;
taskType = selectedSkillRuntime.taskType;
let requiredExecutor = selectedSkillRuntime.requiredExecutor ?? null;
let requiredReviewExecutor = selectedSkillRuntime.requiredReviewExecutor ?? null;
if (!requiredExecutor && !requiredReviewExecutor) {
const pageCursorRuntime = enforcePageGenerationCursorRuntime(userMessage, {
rawToolMode,
taskType,
forceDeepReasoning,
env: process.env,
});
userMessage = pageCursorRuntime.userMessage;
rawToolMode = pageCursorRuntime.rawToolMode;
taskType = pageCursorRuntime.taskType;
if (pageCursorRuntime.requiredExecutor) {
requiredExecutor = pageCursorRuntime.requiredExecutor;
}
}
if (
(selectedSkillRuntime.requiredExecutor || selectedSkillRuntime.requiredReviewExecutor) &&
(requiredExecutor || requiredReviewExecutor) &&
selectedChatSkill(userMessage) === AIDER_DEVELOPMENT_SKILL_NAME &&
!extractAiderDevelopmentTask(userMessage)
) {
response.status(400).json({ message: '请输入需要 Aider 执行的具体开发任务' });
return;
}
if (
(selectedSkillRuntime.requiredExecutor || selectedSkillRuntime.requiredReviewExecutor) &&
(requiredExecutor || requiredReviewExecutor) &&
selectedChatSkill(userMessage) === AIDER_DEVELOPMENT_SKILL_NAME &&
userAuth?.getUserSkills
) {
const skillState = await userAuth.getUserSkills(request.currentUser.id);
@@ -279,9 +302,9 @@ export function createPostAgentRunsHandler({
});
return;
}
if (toolMode === 'code' || selectedSkillRuntime.requiredReviewExecutor) {
if (toolMode === 'code' || requiredReviewExecutor) {
const codeRunPolicy = await resolveCodeRunPolicy(request.currentUser.id);
const policyTaskType = selectedSkillRuntime.requiredReviewExecutor
const policyTaskType = requiredReviewExecutor
? 'h5_chat_code_task'
: taskType;
if (!codeRunPolicy.enabled) {
@@ -305,7 +328,12 @@ export function createPostAgentRunsHandler({
response.status(403).json({ message: '当前代码任务类型未开启灰度' });
return;
}
if (codeRunPolicy.requireValidation && !hasExpectedFileValidation(userMessage)) {
if (
codeRunPolicy.requireValidation
&& !hasExpectedFileValidation(userMessage)
&& !userMessage?.metadata?.memindRun?.pageCursorDefault
&& !userMessage?.metadata?.memindRun?.cursorAgentDefault
) {
response.status(400).json({ message: '代码任务必须声明产物校验规则' });
return;
}
@@ -372,10 +400,12 @@ export function createPostAgentRunsHandler({
response.status(202).json({ run });
} catch (err) {
if (err?.code === 'SESSION_RUN_CONFLICT') {
response.status(409).json({
const payload = {
message: err.message,
code: 'SESSION_RUN_CONFLICT',
});
};
if (err.existingRunId) payload.existingRunId = err.existingRunId;
response.status(409).json(payload);
return;
}
if (err?.code === 'GOAL_RUN_NOT_FOUND') {
+6
View File
@@ -48,11 +48,17 @@ const WEB_NEWS_INTENT_PATTERNS = [
const PAGE_GENERATION_INTENT_PATTERNS = [
/(?:生成|做|制作|创建|设计|写|出)(?:一个|一份|个)?(?:H5|h5|HTML|html|网页|页面|活动页|宣传页|落地页|分享页)/u,
/(?:生成|做|制作|创建|设计|写|出)(?:一个|一份|个)?.{0,40}(?:H5|h5|HTML|html|网页|页面|活动页|宣传页|落地页|分享页)/u,
/(?:帮我|给我).*(?:H5|h5|HTML|html|网页|页面|活动页|宣传页|落地页|分享页)/u,
/(?:publish|create|generate|make|build|design).*(?:html|web\s?page|landing\s?page|h5)/i,
// 隐式页面需求:主题/攻略 + 页面,无显式动词(如「苏州攻略页面」)
/(?:攻略|指南|游记|手册|简介|介绍|展示).{0,8}(?:页面|网页|H5|h5|html)/u,
/[^\s,。!?]{2,20}(?:攻略|指南).{0,4}(?:页面|网页)/u,
// 页面跟进/修改:如「在页面再加几首唐诗」
/(?:在|把|给|对|向).{0,12}(?:页面|网页|这个页|当前页|刚才的页).{0,32}(?:再|继续|追加|加上|添加|加|改|更新|补充|删|调整)/u,
/(?:页面|网页|HTML|html).{0,24}(?:再|继续|追加|加上|添加|加|改|更新|补充|删|调整)/u,
/public\/[\w-]+\.html/u,
/(?:把|更新|修改|改).{0,24}[\w-]+\.html/u,
];
const PRODUCT_CAMPAIGN_INTENT_PATTERNS = [
+3
View File
@@ -212,6 +212,9 @@ test('buildAutoChatSkillPrefix enables publish for page generation requests', ()
test('isPageGenerationIntent matches implicit travel guide page requests', () => {
assert.equal(isPageGenerationIntent('苏州攻略页面'), true);
assert.equal(isPageGenerationIntent('帮我看看苏州攻略,1日攻略,做一个页面'), true);
assert.equal(isPageGenerationIntent('做一个睡前故事页面'), true);
assert.equal(isPageGenerationIntent('在页面再加几首唐诗'), true);
assert.equal(isPageGenerationIntent('把 bedtime-story.html 再改一下'), true);
assert.equal(isPageGenerationIntent('你好'), false);
});
+43 -1
View File
@@ -57,6 +57,17 @@ async function columnExists(pool, table, column) {
return rows.length > 0;
}
async function columnType(pool, table, column) {
const [rows] = await pool.query(
`SELECT COLUMN_TYPE
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?
LIMIT 1`,
[table, column],
);
return rows[0]?.COLUMN_TYPE ?? null;
}
async function tableExists(pool, table) {
const [rows] = await pool.query(
`SELECT 1 FROM information_schema.TABLES
@@ -285,7 +296,7 @@ export async function migrateSchema(pool) {
await pool.query(`
CREATE TABLE IF NOT EXISTS h5_llm_executor_bindings (
id CHAR(36) PRIMARY KEY,
executor ENUM('goose', 'aider', 'openhands') NOT NULL,
executor ENUM('goose', 'aider', 'openhands', 'cursor') NOT NULL,
purpose VARCHAR(32) NOT NULL DEFAULT 'default',
provider_key_id CHAR(36) NULL,
model VARCHAR(128) NOT NULL,
@@ -297,6 +308,13 @@ export async function migrateSchema(pool) {
CONSTRAINT fk_h5_llm_executor_provider FOREIGN KEY (provider_key_id) REFERENCES h5_llm_provider_keys(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
`);
const executorColumnType = await columnType(pool, 'h5_llm_executor_bindings', 'executor');
if (executorColumnType && !String(executorColumnType).includes("'cursor'")) {
await pool.query(
`ALTER TABLE h5_llm_executor_bindings
MODIFY COLUMN executor ENUM('goose', 'aider', 'openhands', 'cursor') NOT NULL`,
);
}
// Optional asset capability control plane. These rows configure no runtime
// worker by themselves; the existing chat/page path remains independent.
@@ -441,6 +459,30 @@ export async function migrateSchema(pool) {
);
}
await pool.query(`
CREATE TABLE IF NOT EXISTS h5_help_escalations (
id CHAR(36) PRIMARY KEY,
user_id CHAR(36) NOT NULL,
channel VARCHAR(32) NOT NULL DEFAULT 'h5',
status ENUM('queued', 'running', 'succeeded', 'failed') NOT NULL DEFAULT 'queued',
user_text MEDIUMTEXT NOT NULL,
context_json JSON NULL,
agent_session_id VARCHAR(128) NULL,
result_text MEDIUMTEXT NULL,
agent_output MEDIUMTEXT NULL,
error_message TEXT NULL,
created_at BIGINT NOT NULL,
updated_at BIGINT NOT NULL,
started_at BIGINT NULL,
completed_at BIGINT NULL,
KEY idx_h5_help_escalation_status_created (status, created_at),
KEY idx_h5_help_escalation_user_created (user_id, created_at),
KEY idx_h5_help_escalation_session (agent_session_id, updated_at),
CONSTRAINT fk_h5_help_escalation_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE,
CONSTRAINT fk_h5_help_escalation_session FOREIGN KEY (agent_session_id) REFERENCES h5_user_sessions(agent_session_id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
`);
await ensureGoalRunSchema(pool, {
columnExists: (table, column) => columnExists(pool, table, column),
indexExists: (table, index) => indexExists(pool, table, index),
+118 -3
View File
@@ -11,6 +11,15 @@ import {
resolveDeepseekNoThinkProxyBaseUrl,
resolveMoonshotCompatProxyBaseUrl,
} from './deepseek-no-think-proxy.mjs';
import {
cursorChatBridgeEnabled,
MEMIND_CURSOR_CHAT_BRIDGE_PROVIDER_ID,
resolveCursorChatBridgeBaseUrl,
} from './cursor-openai-bridge.mjs';
import {
buildCursorExecutorLaunchPlan,
cursorExecutorEnabled,
} from './cursor-agent-launch.mjs';
export const MEMIND_DEEPSEEK_NO_THINK_PROVIDER_ID = 'custom_memind_deepseek_no_think';
@@ -33,6 +42,14 @@ export const LLM_PROVIDER_CATALOG = [
defaultModel: 'deepseek-v4-pro',
models: ['deepseek-v4-pro', 'deepseek-v4-flash'],
},
{
id: 'custom_cursor',
label: 'TKMind Chat Bridge (实验)',
kind: 'builtin',
apiKeyEnv: 'CURSOR_API_KEY',
defaultModel: 'composer-2.5',
models: ['composer-2.5', 'composer-2.5-fast'],
},
{
id: 'openai',
label: 'OpenAI',
@@ -88,6 +105,12 @@ export const LLM_EXECUTOR_CATALOG = [
description: '复杂仓库任务、多文件改造和命令执行',
purposes: ['default'],
},
{
id: 'cursor',
label: 'TKMind 智趣',
description: 'MindSpace 页面与代码任务,通过 TKMind 智趣执行器落盘',
purposes: ['default'],
},
];
const catalogById = Object.fromEntries(LLM_PROVIDER_CATALOG.map((item) => [item.id, item]));
@@ -509,8 +532,20 @@ function resolveExecutorCommand(executor) {
? [process.env.AIDER_BIN, process.env.GOOSE_AIDER_BIN, 'aider']
: executor === 'openhands'
? [process.env.OPENHANDS_BIN, process.env.GOOSE_OPENHANDS_BIN, 'openhands']
: [executor];
return candidates.map((item) => String(item ?? '').trim()).find(Boolean) ?? executor;
: executor === 'cursor'
? [
process.env.MEMIND_CURSOR_EXECUTOR_AGENT_BIN,
process.env.MEMIND_CURSOR_HELP_AGENT_BIN,
process.env.CURSOR_AGENT_BIN,
'~/.local/bin/agent',
'agent',
]
: [executor];
const resolved = candidates.map((item) => String(item ?? '').trim()).find(Boolean) ?? executor;
if (resolved.startsWith('~/')) {
return path.join(os.homedir(), resolved.slice(2));
}
return resolved;
}
function commandExists(command) {
@@ -631,6 +666,7 @@ export function buildExecutorLaunchPlan(runtime, options = {}) {
return {
ok: true,
executor: 'aider',
executorLabel: runtime.executorLabel ?? 'Aider',
cwd,
command,
args: [
@@ -663,6 +699,7 @@ export function buildExecutorLaunchPlan(runtime, options = {}) {
return {
ok: true,
executor: 'openhands',
executorLabel: runtime.executorLabel ?? 'OpenHands',
cwd,
command,
args,
@@ -675,6 +712,21 @@ export function buildExecutorLaunchPlan(runtime, options = {}) {
};
}
if (runtime.executor === 'cursor') {
if (!cursorExecutorEnabled()) {
return {
ok: false,
executor: 'cursor',
message: 'TKMind 智趣执行器未启用(MEMIND_CURSOR_EXECUTOR_ENABLED',
};
}
return buildCursorExecutorLaunchPlan({
cwd,
instruction,
env: process.env,
});
}
return {
ok: false,
executor: runtime.executor,
@@ -868,6 +920,41 @@ async function syncDeepseekNoThinkProfileToGoosed(apiTarget, apiSecret, profile,
return goosedProviderId;
}
async function syncCursorChatBridgeProfileToGoosed(apiTarget, apiSecret, profile, fetchImpl) {
if (!cursorChatBridgeEnabled()) {
throw new Error('TKMind Chat Bridge 未启用(MEMIND_CURSOR_CHAT_BRIDGE_ENABLED=1');
}
const catalogItem = catalogById.custom_cursor;
const models = [
...new Set([
...(Array.isArray(catalogItem?.models) ? catalogItem.models : []),
profile.defaultModel,
...(Array.isArray(profile.models) ? profile.models : []),
].filter(Boolean).map((item) => String(item).trim()).filter(Boolean)),
];
const goosedProviderId = await upsertCustomProviderOnGoosed(
apiTarget,
apiSecret,
{
name: 'memind_cursor_chat_bridge',
goosedProviderId: MEMIND_CURSOR_CHAT_BRIDGE_PROVIDER_ID,
apiUrl: resolveCursorChatBridgeBaseUrl(),
apiKey: profile.apiKey || 'cursor-bridge-local',
models,
defaultModel: profile.defaultModel || catalogItem?.defaultModel,
engine: 'openai',
preservesThinking: false,
},
fetchImpl,
);
await writeGoosedConfig(apiTarget, apiSecret, 'GOOSE_PROVIDER', goosedProviderId, false, fetchImpl);
await writeGoosedConfig(apiTarget, apiSecret, 'GOOSE_MODEL', profile.defaultModel, false, fetchImpl);
await writeGoosedConfig(apiTarget, apiSecret, 'TKMIND_PROVIDER', goosedProviderId, false, fetchImpl);
await writeGoosedConfig(apiTarget, apiSecret, 'TKMIND_MODEL', profile.defaultModel, false, fetchImpl);
await writeGoosedConfig(apiTarget, apiSecret, 'GOOSE_THINKING_EFFORT', 'off', false, fetchImpl);
return goosedProviderId;
}
async function removeCustomProviderOnGoosed(apiTarget, apiSecret, goosedProviderId, fetchImpl) {
if (!goosedProviderId) return;
const upstream = await goosedApiFetch(
@@ -885,6 +972,9 @@ async function removeCustomProviderOnGoosed(apiTarget, apiSecret, goosedProvider
}
async function syncBuiltinProfileToGoosed(apiTarget, apiSecret, profile, fetchImpl) {
if (profile.providerId === 'custom_cursor') {
return syncCursorChatBridgeProfileToGoosed(apiTarget, apiSecret, profile, fetchImpl);
}
if (
profile.providerId === 'custom_deepseek'
&& deepseekDisableThinkingEnabled()
@@ -1195,6 +1285,23 @@ export function createLlmProviderService(
) {
const normalizedExecutor = normalizeExecutor(executor);
if (!normalizedExecutor) return { ok: false, message: '不支持的执行器' };
if (normalizedExecutor === 'cursor') {
if (!cursorExecutorEnabled()) {
return {
ok: false,
executor: 'cursor',
purpose,
message: 'TKMind 智趣执行器未启用(MEMIND_CURSOR_EXECUTOR_ENABLED',
};
}
return {
ok: true,
executor: 'cursor',
executorLabel: executorById.cursor?.label ?? 'TKMind 智趣',
purpose,
env: {},
};
}
const binding = await getExecutorBindingRow(normalizedExecutor, purpose);
if (!binding?.enabled) {
return {
@@ -1431,7 +1538,7 @@ export function createLlmProviderService(
const keys = await this.listKeys();
const keyMap = new Map(keys.map((key) => [key.id, key]));
const [rows] = await pool.query(
'SELECT * FROM h5_llm_executor_bindings ORDER BY FIELD(executor, "goose", "aider", "openhands"), purpose',
'SELECT * FROM h5_llm_executor_bindings ORDER BY FIELD(executor, "goose", "aider", "openhands", "cursor"), purpose',
);
const rowMap = new Map(
rows.map((row) => [`${row.executor}:${row.purpose}`, row]),
@@ -1656,6 +1763,14 @@ export function createLlmProviderService(
};
}
if (normalizedExecutor === 'cursor' && !instruction) {
return {
ok: false,
executor: 'cursor',
message: 'TKMind 智趣启动需要 instruction',
};
}
const runtime = await resolveExecutorRuntimeConfig(normalizedExecutor, {
purpose,
includeSecret: true,
+26 -1
View File
@@ -320,7 +320,7 @@ test('listExecutorBindings returns all executor placeholders', async () => {
const bindings = await service.listExecutorBindings();
assert.deepEqual(
bindings.map((binding) => binding.executor),
['goose', 'aider', 'openhands'],
['goose', 'aider', 'openhands', 'cursor'],
);
assert.equal(bindings[0].enabled, false);
});
@@ -649,6 +649,31 @@ test('buildExecutorLaunchPlan rejects openhands headless without instruction', (
assert.match(plan.message ?? '', /instruction/);
});
test('buildExecutorLaunchPlan creates cursor headless command when enabled', () => {
const previous = process.env.MEMIND_CURSOR_EXECUTOR_ENABLED;
process.env.MEMIND_CURSOR_EXECUTOR_ENABLED = '1';
try {
const plan = buildExecutorLaunchPlan({
ok: true,
executor: 'cursor',
executorLabel: 'TKMind 智趣',
purpose: 'default',
env: {},
}, {
cwd: '/tmp/mindspace/user-1',
instruction: '生成 public/spring-poem.html',
});
assert.equal(plan.ok, true);
assert.equal(plan.executor, 'cursor');
assert.ok(plan.args.includes('--workspace'));
assert.ok(plan.args.includes('/tmp/mindspace/user-1'));
assert.match(plan.args.at(-1) ?? '', /spring-poem\.html/);
} finally {
if (previous === undefined) delete process.env.MEMIND_CURSOR_EXECUTOR_ENABLED;
else process.env.MEMIND_CURSOR_EXECUTOR_ENABLED = previous;
}
});
test('launchExecutor rejects goose launch path', async () => {
const pool = {
async query() {
+1
View File
@@ -113,6 +113,7 @@
"verify:page-data-delivery": "node scripts/repair-page-data-workspace-bindings.mjs --dry-run",
"verify:template-catalog-portal": "node scripts/verify-template-catalog-portal.mjs",
"verify:template-catalog-e2e": "node scripts/verify-template-catalog-e2e.mjs",
"verify:cursor-executor": "node --test cursor-agent-launch.test.mjs cursor-page-routing.test.mjs tool-gateway.test.mjs llm-providers.test.mjs help-escalation.test.mjs executor-display-label.test.mjs agent-run-gateway.test.mjs agent-run-routes.test.mjs wechat-cursor-executor-policy.test.mjs",
"repair:page-data-bindings": "node scripts/repair-page-data-workspace-bindings.mjs",
"repair:page-data:103": "node scripts/ensure-page-data-datasets.mjs && node scripts/repair-page-data-workspace-bindings.mjs",
"verify:wechat-channel-isolation": "node scripts/check-wechat-channel-isolation.mjs",
+23 -1
View File
@@ -622,7 +622,7 @@ CREATE TABLE IF NOT EXISTS h5_llm_provider_keys (
CREATE TABLE IF NOT EXISTS h5_llm_executor_bindings (
id CHAR(36) PRIMARY KEY,
executor ENUM('goose', 'aider', 'openhands') NOT NULL,
executor ENUM('goose', 'aider', 'openhands', 'cursor') NOT NULL,
purpose VARCHAR(32) NOT NULL DEFAULT 'default',
provider_key_id CHAR(36) NULL,
model VARCHAR(128) NOT NULL,
@@ -634,6 +634,28 @@ CREATE TABLE IF NOT EXISTS h5_llm_executor_bindings (
CONSTRAINT fk_h5_llm_executor_provider FOREIGN KEY (provider_key_id) REFERENCES h5_llm_provider_keys(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS h5_help_escalations (
id CHAR(36) PRIMARY KEY,
user_id CHAR(36) NOT NULL,
channel VARCHAR(32) NOT NULL DEFAULT 'h5',
status ENUM('queued', 'running', 'succeeded', 'failed') NOT NULL DEFAULT 'queued',
user_text MEDIUMTEXT NOT NULL,
context_json JSON NULL,
agent_session_id VARCHAR(128) NULL,
result_text MEDIUMTEXT NULL,
agent_output MEDIUMTEXT NULL,
error_message TEXT NULL,
created_at BIGINT NOT NULL,
updated_at BIGINT NOT NULL,
started_at BIGINT NULL,
completed_at BIGINT NULL,
KEY idx_h5_help_escalation_status_created (status, created_at),
KEY idx_h5_help_escalation_user_created (user_id, created_at),
KEY idx_h5_help_escalation_session (agent_session_id, updated_at),
CONSTRAINT fk_h5_help_escalation_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE,
CONSTRAINT fk_h5_help_escalation_session FOREIGN KEY (agent_session_id) REFERENCES h5_user_sessions(agent_session_id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS h5_asset_gateway_config (
config_key VARCHAR(32) PRIMARY KEY,
enabled TINYINT(1) NOT NULL DEFAULT 0,
+22
View File
@@ -13,6 +13,9 @@ import { createManagedMemoryV2Runtime } from '../memory-v2-runtime.mjs';
import { createTkmindProxy } from '../tkmind-proxy.mjs';
import { createToolGateway } from '../tool-gateway.mjs';
import { createUserAuth } from '../user-auth.mjs';
import { createSessionSnapshotService } from '../session-snapshot.mjs';
import { createDirectChatService } from '../direct-chat-service.mjs';
import { createSessionAccess } from '../session-broker.mjs';
import { createOrchestratorAdminConfigService } from '../services/orchestrator/admin-config.mjs';
import { createWorkflowShadowObserver } from '../services/orchestrator/shadow-observer.mjs';
@@ -158,6 +161,21 @@ async function bootstrapWorker() {
err instanceof Error ? err.message : err,
);
});
const sessionSnapshotService = createSessionSnapshotService(pool, {
conversationMemoryService,
memoryV2,
episodicMemoryService,
});
const sessionAccess = createSessionAccess({ userAuth, enabled: false });
const directChatService = createDirectChatService({
userAuth,
sessionAccess,
llmProviderService,
sessionSnapshotService,
memoryV2,
conversationMemoryService,
episodicMemoryService,
});
const chatIntentRouter = createManagedChatIntentRouter({
llmProviderService,
memoryV2,
@@ -176,8 +194,12 @@ async function bootstrapWorker() {
const gateway = createAgentRunGateway({
pool,
userAuth,
sessionAccess,
tkmindProxy,
toolGateway,
directChatService,
sessionSnapshotService,
conversationMemoryService,
chatIntentRouter,
conversationMemoryService,
autoDispatch: false,
+1 -1
View File
@@ -8,7 +8,7 @@ export const MEMIND_RUNTIME_PROFILES = new Set(['local', 'split-service', 'produ
*/
export function loadMemindEnvFiles(rootDir, env = process.env) {
const root = path.resolve(rootDir);
for (const relativePath of ['../../.env.local', '.env']) {
for (const relativePath of ['.env', '.env.local', '../../.env.local']) {
loadEnvFile(path.join(root, relativePath), env);
}
}
+5
View File
@@ -283,6 +283,7 @@ let memoryV2ConfigService = null;
let skillRuntimeConfigService = null;
let systemDisclosurePolicyService = null;
let agentCodeRunPolicyService = null;
let wechatCursorExecutorPolicyService = null;
let wechatScheduleLlmConfigService = null;
let wechatIntentRouter = null;
let mindSpace = null;
@@ -476,6 +477,8 @@ async function bootstrapUserAuth() {
memorySessionServices.systemDisclosurePolicyService;
agentCodeRunPolicyService =
memorySessionServices.agentCodeRunPolicyService;
wechatCursorExecutorPolicyService =
memorySessionServices.wechatCursorExecutorPolicyService;
wechatScheduleLlmConfigService =
memorySessionServices.wechatScheduleLlmConfigService;
wechatIntentRouter =
@@ -543,6 +546,8 @@ async function bootstrapUserAuth() {
sessionSnapshotService,
wechatScheduleLlmConfigService,
wechatIntentRouter,
wechatCursorExecutorPolicyService,
agentRunGateway,
llmProviderService,
chatIntentRouter,
systemDisclosurePolicyService,
@@ -34,6 +34,8 @@ export async function bootstrapPortalIntegrationServices({
sessionSnapshotService = null,
wechatScheduleLlmConfigService,
wechatIntentRouter = null,
wechatCursorExecutorPolicyService = null,
agentRunGateway = null,
llmProviderService,
chatIntentRouter,
systemDisclosurePolicyService,
@@ -119,6 +121,8 @@ export async function bootstrapPortalIntegrationServices({
? mindSpacePublicFinish
: null,
pageDataDeliveryReviewer,
wechatCursorExecutorPolicyService,
agentRunGateway,
apiFetch: tkmindProxy.apiFetch,
startAgentSession: ({
userId,
@@ -13,6 +13,7 @@ import { createSystemDisclosurePolicyService } from '../system-disclosure-policy
import { createWechatScheduleLlmConfigService } from '../wechat-schedule-llm-config.mjs';
import { createWechatIntentRouterConfigService } from '../wechat-intent-router-config.mjs';
import { createManagedWechatIntentRouter } from '../wechat-intent-router.mjs';
import { createWechatCursorExecutorAdminConfigService } from '../wechat-cursor-executor-admin-config.mjs';
export async function bootstrapPortalMemorySessionServices({
pool,
@@ -34,6 +35,8 @@ export async function bootstrapPortalMemorySessionServices({
createWechatScheduleLlmConfigService,
createWechatIntentRouterConfigServiceFn =
createWechatIntentRouterConfigService,
createWechatCursorExecutorAdminConfigServiceFn =
createWechatCursorExecutorAdminConfigService,
createManagedWechatIntentRouterFn =
createManagedWechatIntentRouter,
createConversationMemoryServiceFn =
@@ -79,6 +82,8 @@ export async function bootstrapPortalMemorySessionServices({
createWechatScheduleLlmConfigServiceFn(pool);
const wechatIntentRouterConfigService =
createWechatIntentRouterConfigServiceFn(pool, { env });
const wechatCursorExecutorPolicyService =
createWechatCursorExecutorAdminConfigServiceFn(pool);
const wechatIntentRouter =
createManagedWechatIntentRouterFn({
llmProviderService,
@@ -156,6 +161,7 @@ export async function bootstrapPortalMemorySessionServices({
agentCodeRunPolicyService,
wechatScheduleLlmConfigService,
wechatIntentRouterConfigService,
wechatCursorExecutorPolicyService,
wechatIntentRouter,
conversationMemoryService,
memoryV2,
+12
View File
@@ -47,6 +47,7 @@ export function resolvePageGenerateOutcome({
replyHasPublicLinks = false,
topic = '',
repairContext = {},
wechatCursorChannel = false,
}) {
const context = {
topic: String(topic || repairContext.topic || '').trim(),
@@ -64,6 +65,17 @@ export function resolvePageGenerateOutcome({
return { action: 'send', artifacts: sendable };
}
if (wechatCursorChannel) {
const cursorCandidates = selectSendableHtmlArtifacts({
verifiedArtifacts,
confirmedArtifacts,
publishDir: context.publishDir,
}).filter((artifact) => verifyPageArtifactContent(artifact, previewOptions).ok);
if (cursorCandidates.length > 0) {
return { action: 'send', artifacts: cursorCandidates };
}
}
if (
suspiciousPublishClaim ||
bareCompletionReply ||