feat(chat): message accumulation + initial full scroll scrape — history persistence
This commit is contained in:
@@ -21,12 +21,79 @@ class CDPClient {
|
|||||||
this.port = port;
|
this.port = port;
|
||||||
this.client = null;
|
this.client = null;
|
||||||
this.connected = false;
|
this.connected = false;
|
||||||
this.lastChatHTML = '';
|
|
||||||
this.pollInterval = null;
|
this.pollInterval = null;
|
||||||
this.onChatUpdate = null; // callback(html)
|
this.onChatUpdate = null; // callback(messages[])
|
||||||
this.onDisconnect = null; // callback()
|
this.onDisconnect = null; // callback()
|
||||||
this.onScreencastFrame = null; // callback({data, metadata})
|
this.onScreencastFrame = null; // callback({data, metadata})
|
||||||
this.screencastRunning = false;
|
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) {
|
startPolling(intervalMs = 1000) {
|
||||||
this.stopPolling();
|
this.stopPolling();
|
||||||
|
|
||||||
|
// 초기 전체 스캔 (한 번만)
|
||||||
|
if (!this.initialScrapeComplete) {
|
||||||
|
this._initialFullScrape().then(() => {
|
||||||
|
this.initialScrapeComplete = true;
|
||||||
|
console.log(`[CDP] 초기 전체 스캔 완료: ${this.messageHistory.length}개 메시지`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.pollInterval = setInterval(async () => {
|
this.pollInterval = setInterval(async () => {
|
||||||
if (!this.connected) {
|
if (!this.connected) {
|
||||||
this.stopPolling();
|
this.stopPolling();
|
||||||
@@ -375,11 +450,9 @@ class CDPClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const messages = await this.scrapeChatDOM();
|
const messages = await this.scrapeChatDOM();
|
||||||
const hash = JSON.stringify(messages);
|
if (this._accumulateMessages(messages)) {
|
||||||
if (messages && messages.length > 0 && hash !== this.lastChatHTML) {
|
|
||||||
this.lastChatHTML = hash;
|
|
||||||
if (this.onChatUpdate) {
|
if (this.onChatUpdate) {
|
||||||
this.onChatUpdate(messages);
|
this.onChatUpdate([...this.messageHistory]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, intervalMs);
|
}, intervalMs);
|
||||||
@@ -387,6 +460,78 @@ class CDPClient {
|
|||||||
console.log(`[CDP] 폴링 시작 (${intervalMs}ms 간격)`);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 채팅 폴링 중지
|
* 채팅 폴링 중지
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user