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:
@@ -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,
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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
@@ -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') {
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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() {
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 ||
|
||||
|
||||
Reference in New Issue
Block a user