From d1d17bdb7aeea068442370879388ad7a0975294e Mon Sep 17 00:00:00 2001 From: Variet Date: Sat, 7 Mar 2026 23:08:19 +0900 Subject: [PATCH] =?UTF-8?q?feat(chat):=20message=20accumulation=20+=20init?= =?UTF-8?q?ial=20full=20scroll=20scrape=20=E2=80=94=20history=20persistenc?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/cdp-client.js | 157 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 151 insertions(+), 6 deletions(-) diff --git a/server/cdp-client.js b/server/cdp-client.js index 401632f..5c5d7d4 100644 --- a/server/cdp-client.js +++ b/server/cdp-client.js @@ -21,12 +21,79 @@ class CDPClient { this.port = port; this.client = null; this.connected = false; - this.lastChatHTML = ''; this.pollInterval = null; - this.onChatUpdate = null; // callback(html) + this.onChatUpdate = null; // callback(messages[]) this.onDisconnect = null; // callback() this.onScreencastFrame = null; // callback({data, metadata}) this.screencastRunning = false; + + // 메시지 누적 시스템 + this.messageHistory = []; // 전체 누적 메시지 + this.seenHashes = new Set(); // 중복 제거용 해시 + this.initialScrapeComplete = false; + } + + /** + * 메시지 해시 생성 (중복 제거용) + */ + _msgHash(msg) { + if (msg.type === 'task') return `task:${msg.title}:${msg.summary?.substring(0, 50) || ''}`; + if (msg.type === 'user') return `user:${msg.content?.substring(0, 100) || ''}`; + if (msg.type === 'text') return `text:${msg.content?.substring(0, 100) || ''}`; + if (msg.type === 'thought') return `thought:${msg.label || ''}`; + if (msg.type === 'status') return `status:${msg.content || ''}`; + return `${msg.type}:${JSON.stringify(msg).substring(0, 100)}`; + } + + /** + * 새 메시지들을 히스토리에 누적 (중복 제거) + * @returns {boolean} 새 메시지가 추가되었는지 + */ + _accumulateMessages(newMessages) { + if (!newMessages || newMessages.length === 0) return false; + + let added = false; + + // status 타입은 항상 마지막 것만 유지 (변경될 수 있음) + const statusMsgs = newMessages.filter(m => m.type === 'status'); + const nonStatusMsgs = newMessages.filter(m => m.type !== 'status'); + + for (const msg of nonStatusMsgs) { + const hash = this._msgHash(msg); + if (!this.seenHashes.has(hash)) { + this.seenHashes.add(hash); + // task 카드의 actions/steps는 업데이트될 수 있음 → 기존 것 교체 + if (msg.type === 'task') { + const existIdx = this.messageHistory.findIndex(m => + m.type === 'task' && m.title === msg.title + ); + if (existIdx >= 0) { + this.messageHistory[existIdx] = msg; + added = true; + continue; + } + } + this.messageHistory.push(msg); + added = true; + } + } + + // status는 마지막 것으로 교체 + if (statusMsgs.length > 0) { + const lastStatus = statusMsgs[statusMsgs.length - 1]; + const existStatusIdx = this.messageHistory.findIndex(m => m.type === 'status'); + if (existStatusIdx >= 0) { + if (this.messageHistory[existStatusIdx].content !== lastStatus.content) { + this.messageHistory[existStatusIdx] = lastStatus; + added = true; + } + } else { + this.messageHistory.push(lastStatus); + added = true; + } + } + + return added; } /** @@ -368,6 +435,14 @@ class CDPClient { startPolling(intervalMs = 1000) { this.stopPolling(); + // 초기 전체 스캔 (한 번만) + if (!this.initialScrapeComplete) { + this._initialFullScrape().then(() => { + this.initialScrapeComplete = true; + console.log(`[CDP] 초기 전체 스캔 완료: ${this.messageHistory.length}개 메시지`); + }); + } + this.pollInterval = setInterval(async () => { if (!this.connected) { this.stopPolling(); @@ -375,11 +450,9 @@ class CDPClient { } const messages = await this.scrapeChatDOM(); - const hash = JSON.stringify(messages); - if (messages && messages.length > 0 && hash !== this.lastChatHTML) { - this.lastChatHTML = hash; + if (this._accumulateMessages(messages)) { if (this.onChatUpdate) { - this.onChatUpdate(messages); + this.onChatUpdate([...this.messageHistory]); } } }, intervalMs); @@ -387,6 +460,78 @@ class CDPClient { console.log(`[CDP] 폴링 시작 (${intervalMs}ms 간격)`); } + /** + * 초기 전체 스크롤 스캔 — Antigravity 대화를 top→bottom으로 순회하며 전체 수집 + */ + async _initialFullScrape() { + if (!this.connected || !this.client) return; + + try { + // 스크롤 정보 가져오기 + const { result: scrollInfo } = await this.client.Runtime.evaluate({ + expression: ` + (function() { + const conv = document.querySelector('#conversation'); + if (!conv) return JSON.stringify(null); + const sc = conv.querySelector('.overflow-y-auto'); + if (!sc) return JSON.stringify(null); + return JSON.stringify({ + scrollTop: sc.scrollTop, + scrollHeight: sc.scrollHeight, + clientHeight: sc.clientHeight, + }); + })() + `, + returnByValue: true, + }); + + const info = JSON.parse(scrollInfo.value); + if (!info) return; + + const savedScrollTop = info.scrollTop; + const totalHeight = info.scrollHeight; + const viewHeight = info.clientHeight; + const steps = Math.ceil(totalHeight / (viewHeight * 0.8)); // 80% 겹침두며 스크롤 + + console.log(`[CDP] 전체 스캔 시작: ${totalHeight}px, ${steps}단계`); + + // top으로 이동 + await this.client.Runtime.evaluate({ + expression: `document.querySelector('#conversation .overflow-y-auto').scrollTop = 0;`, + }); + await new Promise(r => setTimeout(r, 300)); + + // 상단부터 하단까지 스크래핑 + for (let step = 0; step < steps; step++) { + const messages = await this.scrapeChatDOM(); + this._accumulateMessages(messages); + + // 80% 단위로 스크롤 + await this.client.Runtime.evaluate({ + expression: ` + const sc = document.querySelector('#conversation .overflow-y-auto'); + sc.scrollTop += Math.round(sc.clientHeight * 0.8); + `, + }); + await new Promise(r => setTimeout(r, 200)); + } + + // 원래 스크롤 위치 복원 + await this.client.Runtime.evaluate({ + expression: `document.querySelector('#conversation .overflow-y-auto').scrollTop = ${savedScrollTop};`, + }); + + // 초기 수집 결과 전송 + if (this.messageHistory.length > 0 && this.onChatUpdate) { + this.onChatUpdate([...this.messageHistory]); + } + + console.log(`[CDP] 전체 스캔 결과: ${this.messageHistory.length}개 메시지 수집`); + } catch (err) { + console.error('[CDP] 전체 스캔 오류:', err.message); + } + } + /** * 채팅 폴링 중지 */