From 016da2e67261edab65f6734874a2966c7917312e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=BB=BA=E6=96=8C?= Date: Thu, 2 Apr 2026 00:33:34 +0800 Subject: [PATCH] =?UTF-8?q?feat(cursor):=20=E6=96=B0=E5=A2=9E=20chat=20?= =?UTF-8?q?=E5=91=BD=E4=BB=A4=20=E2=80=94=20=E6=96=B0=E5=BB=BA=E5=AF=B9?= =?UTF-8?q?=E8=AF=9D=E5=B9=B6=E9=80=9A=E8=BF=87=20CDP=20=E5=8E=9F=E7=94=9F?= =?UTF-8?q?=E4=BA=8B=E4=BB=B6=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 使用 Cmd+N 打开新对话窗口(而非 Cmd+L) - 通过 CDP Input.dispatchKeyEvent 发送 Enter 键,解决 Cursor v2.6.22 Lexical 编辑器不响应 JS dispatchEvent 合成事件的问题 - 支持 --timeout 参数控制等待 AI 回复的超时时间(默认 60s) - 从 [data-message-role="ai"] 的 .markdown-root 提取回复文本 Made-with: Cursor --- src/clis/cursor/chat.ts | 135 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 src/clis/cursor/chat.ts diff --git a/src/clis/cursor/chat.ts b/src/clis/cursor/chat.ts new file mode 100644 index 00000000..033d7196 --- /dev/null +++ b/src/clis/cursor/chat.ts @@ -0,0 +1,135 @@ +import { cli, Strategy } from '../../registry.js'; +import { SelectorError } from '../../errors.js'; +import type { IPage } from '../../types.js'; + +export const chatCommand = cli({ + site: 'cursor', + name: 'chat', + description: 'Open a new Cursor chat and send a prompt via CDP native key events', + domain: 'localhost', + strategy: Strategy.UI, + browser: true, + args: [ + { name: 'text', required: true, positional: true, help: 'Prompt to send' }, + { name: 'timeout', required: false, help: 'Max seconds to wait for response (default: 60)', default: '60' }, + ], + columns: ['Role', 'Text'], + func: async (page: IPage, kwargs: any) => { + const text = kwargs.text as string; + const timeout = parseInt(kwargs.timeout as string, 10) || 60; + const isMac = process.platform === 'darwin'; + + // Cmd+N 打开新对话 + await cdpKeyCombo(page, isMac ? 'Meta' : 'Control', 'n'); + await page.wait(2); + + // 查找输入框并注入文本 + const injected = await page.evaluate( + `(function(text) { + let editor = document.querySelector('.aislash-editor-input, [data-lexical-editor="true"], [contenteditable="true"]'); + if (!editor) return false; + editor.focus(); + document.execCommand('selectAll'); + document.execCommand('delete'); + document.execCommand('insertText', false, text); + return true; + })(${JSON.stringify(text)})` + ); + + if (!injected) { + throw new SelectorError('Cursor chat input element'); + } + + await page.wait(0.5); + + // 用 CDP 原生 Input.dispatchKeyEvent 发送 Enter(JS dispatchEvent 无法触发 Lexical 提交) + await cdpPressEnter(page); + await page.wait(3); + + // 轮询等待 AI 回复(新对话从 0 条消息开始) + const pollInterval = 2; + const maxPolls = Math.ceil(timeout / pollInterval); + let response = ''; + + for (let i = 0; i < maxPolls; i++) { + await page.wait(pollInterval); + const result = await page.evaluate(` + (function() { + const msgs = document.querySelectorAll('[data-message-role]'); + for (let j = msgs.length - 1; j >= 0; j--) { + const role = msgs[j].getAttribute('data-message-role'); + if (role === 'ai' || role === 'assistant') { + const root = msgs[j].querySelector('.markdown-root'); + const text = root ? root.innerText : msgs[j].innerText; + return text ? text.trim() : null; + } + } + return null; + })() + `); + if (result) { + response = result; + break; + } + } + + if (!response) { + return [ + { Role: 'User', Text: text }, + { Role: 'System', Text: `No response received within ${timeout}s. The AI may still be generating.` }, + ]; + } + + return [ + { Role: 'User', Text: text }, + { Role: 'Assistant', Text: response }, + ]; + }, +}); + +function getBridge(page: IPage): any { + return (page as any).bridge; +} + +async function cdpPressEnter(page: IPage): Promise { + const bridge = getBridge(page); + await bridge.send('Input.dispatchKeyEvent', { + type: 'keyDown', + key: 'Enter', + code: 'Enter', + windowsVirtualKeyCode: 13, + nativeVirtualKeyCode: 13, + }); + await bridge.send('Input.dispatchKeyEvent', { + type: 'keyUp', + key: 'Enter', + code: 'Enter', + windowsVirtualKeyCode: 13, + nativeVirtualKeyCode: 13, + }); +} + +async function cdpKeyCombo(page: IPage, modifier: string, key: string): Promise { + const bridge = getBridge(page); + const modFlag = modifier === 'Meta' ? 4 : modifier === 'Control' ? 2 : modifier === 'Alt' ? 1 : 0; + const modCode = modifier === 'Meta' ? 'MetaLeft' : modifier === 'Control' ? 'ControlLeft' : 'AltLeft'; + + await bridge.send('Input.dispatchKeyEvent', { + type: 'keyDown', key: modifier, code: modCode, modifiers: modFlag, + }); + await bridge.send('Input.dispatchKeyEvent', { + type: 'keyDown', key, code: 'Key' + key.toUpperCase(), + windowsVirtualKeyCode: key.toUpperCase().charCodeAt(0), + nativeVirtualKeyCode: key.toUpperCase().charCodeAt(0), + modifiers: modFlag, + }); + await bridge.send('Input.dispatchKeyEvent', { + type: 'keyUp', key, code: 'Key' + key.toUpperCase(), + windowsVirtualKeyCode: key.toUpperCase().charCodeAt(0), + nativeVirtualKeyCode: key.toUpperCase().charCodeAt(0), + modifiers: modFlag, + }); + await bridge.send('Input.dispatchKeyEvent', { + type: 'keyUp', key: modifier, code: modCode, + }); +}