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:
+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,
|
||||
|
||||
Reference in New Issue
Block a user