Files
memind/cursor-openai-bridge.mjs
T
john 8f91c52c67 feat(help): add cursor help escalation worker and chat bridge
Introduce help escalation persistence, cursor help worker processing,
optional OpenAI-compatible chat bridge, and page delivery remediation.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-27 09:33:49 +08:00

234 lines
7.3 KiB
JavaScript

/**
* Experimental OpenAI-compatible bridge: goosed -> Cursor agent CLI.
*
* Limitations (PoC):
* - No tool calls / function calling
* - Streaming is best-effort single-chunk
* - High latency (spawns agent per request)
*
* Enable: MEMIND_CURSOR_CHAT_BRIDGE_ENABLED=1
* Listen: MEMIND_CURSOR_CHAT_BRIDGE_PORT=18040 (default)
*/
import http from 'node:http';
import crypto from 'node:crypto';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { runCursorHelpAgent, resolveCursorHelpWorkspace, resolveCursorHelpAgentBin } from './help-escalation.mjs';
export const DEFAULT_CURSOR_CHAT_BRIDGE_PORT = 18040;
export const MEMIND_CURSOR_CHAT_BRIDGE_PROVIDER_ID = 'custom_memind_cursor_chat_bridge';
function envFlag(value, fallback = false) {
const raw = String(value ?? '').trim().toLowerCase();
if (!raw) return fallback;
return ['1', 'true', 'yes', 'on'].includes(raw);
}
export function cursorChatBridgeEnabled(env = process.env) {
return envFlag(env.MEMIND_CURSOR_CHAT_BRIDGE_ENABLED, false);
}
export function resolveCursorChatBridgeListenPort(env = process.env) {
const port = Number(env.MEMIND_CURSOR_CHAT_BRIDGE_PORT ?? DEFAULT_CURSOR_CHAT_BRIDGE_PORT);
return Number.isFinite(port) && port > 0 ? port : DEFAULT_CURSOR_CHAT_BRIDGE_PORT;
}
export function resolveCursorChatBridgeBaseUrl(env = process.env) {
const explicit = String(env.MEMIND_CURSOR_CHAT_BRIDGE_BASE_URL ?? '').trim().replace(/\/$/, '');
if (explicit) return explicit;
const host = String(
env.MEMIND_CURSOR_CHAT_BRIDGE_HOST
?? env.MEMIND_GOOSED_HOST_GATEWAY
?? '127.0.0.1',
).trim();
return `http://${host}:${resolveCursorChatBridgeListenPort(env)}/v1`;
}
function readRequestBody(req) {
return new Promise((resolve, reject) => {
const chunks = [];
req.on('data', (chunk) => chunks.push(chunk));
req.on('end', () => resolve(Buffer.concat(chunks)));
req.on('error', reject);
});
}
function buildPromptFromChatCompletions(body) {
const messages = Array.isArray(body?.messages) ? body.messages : [];
const lines = [
'你是 TKMind 助手,通过 Goose 聊天桥接被调用。请用中文简洁回答用户。',
'不要调用工具;若用户要做页面/写文件,给出下一步建议即可。',
'',
];
for (const message of messages) {
const role = String(message?.role ?? 'user');
const content = typeof message?.content === 'string'
? message.content
: Array.isArray(message?.content)
? message.content
.map((item) => (item?.type === 'text' ? String(item.text ?? '') : ''))
.filter(Boolean)
.join('\n')
: '';
if (!content.trim()) continue;
lines.push(`${role}: ${content.trim()}`);
}
lines.push('assistant:');
return lines.join('\n');
}
function openAiChatCompletionResponse({ model, content }) {
const created = Math.floor(Date.now() / 1000);
const id = `chatcmpl-cursor-${crypto.randomUUID()}`;
return {
id,
object: 'chat.completion',
created,
model: model ?? 'composer-2.5',
choices: [{
index: 0,
message: { role: 'assistant', content: String(content ?? '').trim() },
finish_reason: 'stop',
}],
usage: {
prompt_tokens: 0,
completion_tokens: 0,
total_tokens: 0,
},
};
}
export function createCursorChatBridgeHandler({ env = process.env, logger = console } = {}) {
const workspace = resolveCursorHelpWorkspace(env);
const agentBin = resolveCursorHelpAgentBin(env);
const timeoutMs = Number(env.MEMIND_CURSOR_CHAT_BRIDGE_TIMEOUT_MS ?? 300_000);
return async function handle(req, res) {
if (req.method === 'GET' && req.url === '/health') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ ok: true, bridge: 'cursor-openai' }));
return;
}
if (req.method !== 'POST' || !/\/chat\/completions\/?$/i.test(String(req.url ?? ''))) {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: { message: 'Not found' } }));
return;
}
const raw = await readRequestBody(req);
let body = {};
try {
body = raw.length ? JSON.parse(raw.toString('utf8')) : {};
} catch {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: { message: 'Invalid JSON body' } }));
return;
}
if (Array.isArray(body.tools) && body.tools.length > 0) {
res.writeHead(501, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
error: {
message: 'Cursor chat bridge PoC does not support tool calls yet; keep Goose on DeepSeek for tool turns.',
},
}));
return;
}
const prompt = buildPromptFromChatCompletions(body);
logger.info?.('[cursor-chat-bridge] request', {
model: body?.model ?? null,
promptChars: prompt.length,
});
const { stdout } = await runCursorHelpAgent({
prompt,
workspace,
agentBin,
env: {
...env,
MEMIND_CURSOR_HELP_TIMEOUT_MS: String(timeoutMs),
},
logger,
});
const payload = openAiChatCompletionResponse({
model: body?.model,
content: stdout,
});
if (body.stream === true) {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
});
const chunk = {
id: payload.id,
object: 'chat.completion.chunk',
created: payload.created,
model: payload.model,
choices: [{
index: 0,
delta: { role: 'assistant', content: payload.choices[0].message.content },
finish_reason: null,
}],
};
res.write(`data: ${JSON.stringify(chunk)}\n\n`);
res.write(`data: ${JSON.stringify({
...chunk,
choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],
})}\n\n`);
res.write('data: [DONE]\n\n');
res.end();
return;
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(payload));
};
}
export function startCursorChatBridgeServer({
port = resolveCursorChatBridgeListenPort(),
host = '0.0.0.0',
env = process.env,
logger = console,
} = {}) {
const handler = createCursorChatBridgeHandler({ env, logger });
const server = http.createServer((req, res) => {
handler(req, res).catch((error) => {
logger.error?.('[cursor-chat-bridge] failed', error instanceof Error ? error.message : error);
if (!res.headersSent) {
res.writeHead(502, { 'Content-Type': 'application/json' });
}
res.end(JSON.stringify({
error: { message: error instanceof Error ? error.message : 'cursor chat bridge failed' },
}));
});
});
return new Promise((resolve, reject) => {
server.once('error', reject);
server.listen(port, host, () => {
logger.info?.(`[cursor-chat-bridge] listening on http://${host}:${port}/v1`);
resolve(server);
});
});
}
const isMain = Boolean(
process.env.MEMIND_CURSOR_CHAT_BRIDGE_ENTRYPOINT === '1'
&& process.argv[1]
&& fileURLToPath(import.meta.url) === path.resolve(process.argv[1]),
);
if (isMain) {
if (!cursorChatBridgeEnabled()) {
console.error('MEMIND_CURSOR_CHAT_BRIDGE_ENABLED is not set');
process.exit(1);
}
await startCursorChatBridgeServer();
}