feat(chat): message accumulation + initial full scroll scrape — history persistence

This commit is contained in:
2026-03-07 23:08:19 +09:00
parent 9281c6b45d
commit d1d17bdb7a

View File

@@ -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);
}
}
/**
* 채팅 폴링 중지
*/