fix(cdp): update DOM selectors to match actual Antigravity v1.107 structure
This commit is contained in:
@@ -12,7 +12,7 @@ description: Gravity Web 프로젝트 연동 서비스 URL, API 키, 프로젝
|
|||||||
| 항목 | 값 |
|
| 항목 | 값 |
|
||||||
|------|-----|
|
|------|-----|
|
||||||
| **Node.js** | 시스템 설치 (`node`, `npm`) |
|
| **Node.js** | 시스템 설치 (`node`, `npm`) |
|
||||||
| **Python (helper)** | `C:\ProgramData\miniforge3\envs\deriva\python.exe` |
|
| **Python (helper)** | `C:\ProgramData\miniforge3\envs\gravity_web\python.exe` |
|
||||||
| **프로젝트 루트** | `c:\Users\Certes\Desktop\gravity_web` |
|
| **프로젝트 루트** | `c:\Users\Certes\Desktop\gravity_web` |
|
||||||
| **Shell** | PowerShell (`curl` = `Invoke-WebRequest` 별칭이므로 반드시 **`curl.exe`** 사용) |
|
| **Shell** | PowerShell (`curl` = `Invoke-WebRequest` 별칭이므로 반드시 **`curl.exe`** 사용) |
|
||||||
| **서버 실행** | `cd server && cmd /c node index.js` (port 3300) |
|
| **서버 실행** | `cd server && cmd /c node index.js` (port 3300) |
|
||||||
|
|||||||
@@ -2,6 +2,15 @@
|
|||||||
* CDP Client — Antigravity IDE와 Chrome DevTools Protocol로 통신
|
* CDP Client — Antigravity IDE와 Chrome DevTools Protocol로 통신
|
||||||
*
|
*
|
||||||
* 채팅 DOM 스크래핑, 메시지 전송, 상태 모니터링
|
* 채팅 DOM 스크래핑, 메시지 전송, 상태 모니터링
|
||||||
|
*
|
||||||
|
* Antigravity DOM 구조 (v1.107.0 기준):
|
||||||
|
* .antigravity-agent-side-panel
|
||||||
|
* └ #conversation ← 대화 스크롤 영역
|
||||||
|
* ├ .overflow-y-auto ← 메시지 스크롤 컨테이너
|
||||||
|
* │ └ (메시지 블록들)
|
||||||
|
* └ .text-ide-message-block-bot-color ← 입력 영역 래퍼
|
||||||
|
* └ #antigravity.agentSidePanelInputBox
|
||||||
|
* └ [contenteditable][role="textbox"] ← 입력창
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const CDP = require('chrome-remote-interface');
|
const CDP = require('chrome-remote-interface');
|
||||||
@@ -25,10 +34,14 @@ class CDPClient {
|
|||||||
try {
|
try {
|
||||||
// CDP 타겟 목록에서 올바른 페이지 찾기
|
// CDP 타겟 목록에서 올바른 페이지 찾기
|
||||||
const targets = await CDP.List({ host: this.host, port: this.port });
|
const targets = await CDP.List({ host: this.host, port: this.port });
|
||||||
|
|
||||||
// Antigravity 메인 윈도우 타겟 찾기 (type: 'page')
|
// Antigravity 메인 워크벤치 타겟 (jetski-agent 런치패드 제외)
|
||||||
const pageTarget = targets.find(t => t.type === 'page' && !t.url.includes('devtools'));
|
const pageTarget = targets.find(t =>
|
||||||
|
t.type === 'page' &&
|
||||||
|
t.url.includes('workbench.html') &&
|
||||||
|
!t.url.includes('jetski')
|
||||||
|
) || targets.find(t => t.type === 'page' && !t.url.includes('devtools'));
|
||||||
|
|
||||||
if (!pageTarget) {
|
if (!pageTarget) {
|
||||||
throw new Error('Antigravity 페이지 타겟을 찾을 수 없습니다');
|
throw new Error('Antigravity 페이지 타겟을 찾을 수 없습니다');
|
||||||
}
|
}
|
||||||
@@ -39,8 +52,8 @@ class CDPClient {
|
|||||||
target: pageTarget,
|
target: pageTarget,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { Runtime, DOM, Page } = this.client;
|
const { Runtime, DOM, Page, Input } = this.client;
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
Runtime.enable(),
|
Runtime.enable(),
|
||||||
DOM.enable(),
|
DOM.enable(),
|
||||||
@@ -82,8 +95,7 @@ class CDPClient {
|
|||||||
/**
|
/**
|
||||||
* 채팅 영역의 DOM을 스크래핑
|
* 채팅 영역의 DOM을 스크래핑
|
||||||
*
|
*
|
||||||
* Antigravity의 채팅 컨테이너를 JS로 탐색하여 HTML 반환
|
* Antigravity의 에이전트 사이드 패널에서 대화 내용을 추출
|
||||||
* DOM 셀렉터는 실제 Antigravity 버전에 따라 조정 필요
|
|
||||||
*/
|
*/
|
||||||
async scrapeChatDOM() {
|
async scrapeChatDOM() {
|
||||||
if (!this.connected || !this.client) return null;
|
if (!this.connected || !this.client) return null;
|
||||||
@@ -92,13 +104,10 @@ class CDPClient {
|
|||||||
const { result } = await this.client.Runtime.evaluate({
|
const { result } = await this.client.Runtime.evaluate({
|
||||||
expression: `
|
expression: `
|
||||||
(function() {
|
(function() {
|
||||||
// Antigravity 채팅 컨테이너 셀렉터들 (우선순위 순)
|
// Antigravity 실제 DOM 셀렉터 (우선순위 순)
|
||||||
const selectors = [
|
const selectors = [
|
||||||
'.chat-messages-container',
|
'#conversation',
|
||||||
'[class*="chat"][class*="container"]',
|
'.antigravity-agent-side-panel',
|
||||||
'[class*="conversation"]',
|
|
||||||
'[class*="messages"]',
|
|
||||||
'.monaco-workbench .part.panel .content',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const sel of selectors) {
|
for (const sel of selectors) {
|
||||||
@@ -108,8 +117,7 @@ class CDPClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 폴백: body 전체(디버깅용)
|
return '<!-- agent chat panel not found -->';
|
||||||
return '<!-- chat container not found -->';
|
|
||||||
})()
|
})()
|
||||||
`,
|
`,
|
||||||
returnByValue: true,
|
returnByValue: true,
|
||||||
@@ -124,6 +132,8 @@ class CDPClient {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Antigravity 채팅 입력창에 메시지를 전송
|
* Antigravity 채팅 입력창에 메시지를 전송
|
||||||
|
*
|
||||||
|
* Antigravity는 textarea가 아닌 contenteditable div를 사용함
|
||||||
*/
|
*/
|
||||||
async sendMessage(text) {
|
async sendMessage(text) {
|
||||||
if (!this.connected || !this.client) {
|
if (!this.connected || !this.client) {
|
||||||
@@ -131,24 +141,23 @@ class CDPClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1) 입력창 찾기 및 포커스
|
// 1) contenteditable 입력창 찾기 및 포커스
|
||||||
const { result: focusResult } = await this.client.Runtime.evaluate({
|
const { result: focusResult } = await this.client.Runtime.evaluate({
|
||||||
expression: `
|
expression: `
|
||||||
(function() {
|
(function() {
|
||||||
|
// Antigravity 입력창 셀렉터 (우선순위 순)
|
||||||
const selectors = [
|
const selectors = [
|
||||||
'textarea[class*="chat"]',
|
'#antigravity\\\\.agentSidePanelInputBox [contenteditable="true"][role="textbox"]',
|
||||||
'[class*="chat"] textarea',
|
'.antigravity-agent-side-panel [contenteditable="true"][role="textbox"]',
|
||||||
'[class*="input"] textarea',
|
'#conversation [contenteditable="true"]',
|
||||||
'textarea[placeholder]',
|
'[contenteditable="true"][role="textbox"]',
|
||||||
'.chat-input textarea',
|
|
||||||
'[contenteditable="true"]',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const sel of selectors) {
|
for (const sel of selectors) {
|
||||||
const el = document.querySelector(sel);
|
const el = document.querySelector(sel);
|
||||||
if (el) {
|
if (el) {
|
||||||
el.focus();
|
el.focus();
|
||||||
return { found: true, tag: el.tagName, sel: sel };
|
return { found: true, sel: sel };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { found: false };
|
return { found: false };
|
||||||
@@ -161,25 +170,24 @@ class CDPClient {
|
|||||||
return { success: false, error: '채팅 입력창을 찾을 수 없습니다' };
|
return { success: false, error: '채팅 입력창을 찾을 수 없습니다' };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) 텍스트 입력 (clipboardData 방식 - 가장 범용적)
|
// 2) 텍스트 입력 (contenteditable용 — execCommand 방식)
|
||||||
await this.client.Runtime.evaluate({
|
await this.client.Runtime.evaluate({
|
||||||
expression: `
|
expression: `
|
||||||
(function() {
|
(function() {
|
||||||
const el = document.querySelector('${focusResult.value.sel}');
|
const el = document.querySelector('${focusResult.value.sel}');
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
|
|
||||||
// contenteditable인 경우
|
el.focus();
|
||||||
if (el.contentEditable === 'true') {
|
|
||||||
el.textContent = ${JSON.stringify(text)};
|
// 기존 내용 선택 후 삭제
|
||||||
el.dispatchEvent(new Event('input', { bubbles: true }));
|
const selection = window.getSelection();
|
||||||
} else {
|
const range = document.createRange();
|
||||||
// textarea인 경우 — React setState 호환
|
range.selectNodeContents(el);
|
||||||
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
|
selection.removeAllRanges();
|
||||||
window.HTMLTextAreaElement.prototype, 'value'
|
selection.addRange(range);
|
||||||
).set;
|
|
||||||
nativeInputValueSetter.call(el, ${JSON.stringify(text)});
|
// 텍스트 삽입 (React/Preact 호환)
|
||||||
el.dispatchEvent(new Event('input', { bubbles: true }));
|
document.execCommand('insertText', false, ${JSON.stringify(text)});
|
||||||
}
|
|
||||||
})()
|
})()
|
||||||
`,
|
`,
|
||||||
returnByValue: true,
|
returnByValue: true,
|
||||||
@@ -214,7 +222,7 @@ class CDPClient {
|
|||||||
*/
|
*/
|
||||||
startPolling(intervalMs = 1000) {
|
startPolling(intervalMs = 1000) {
|
||||||
this.stopPolling();
|
this.stopPolling();
|
||||||
|
|
||||||
this.pollInterval = setInterval(async () => {
|
this.pollInterval = setInterval(async () => {
|
||||||
if (!this.connected) {
|
if (!this.connected) {
|
||||||
this.stopPolling();
|
this.stopPolling();
|
||||||
|
|||||||
Reference in New Issue
Block a user