From eb4bef07448edf6497f2e741a73d348cd579d682 Mon Sep 17 00:00:00 2001 From: g1nation Date: Fri, 5 Jun 2026 18:39:41 +0900 Subject: [PATCH] =?UTF-8?q?feat(astra):=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20Se?= =?UTF-8?q?ttings=20=ED=8C=A8=EB=84=90=20=EC=84=B9=EC=85=98=20(=EC=9E=90?= =?UTF-8?q?=EB=8F=99=EB=8F=99=EA=B8=B0=ED=99=94=20=ED=86=A0=EA=B8=80=20+?= =?UTF-8?q?=20=EC=A7=80=EA=B8=88=20=EB=8F=99=EA=B8=B0=ED=99=94=20+=20?= =?UTF-8?q?=EC=9D=B8=EB=8D=B1=EC=8A=A4=20=EC=83=81=ED=83=9C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Astra Settings 패널에 Email 섹션 추가 — autoSync 토글, 간격/범위/최대수 설정, '지금 동기화' 버튼(슬래시와 동일 syncEmails 코어), 인덱스 상태(건수/최신일) 표시. VSCode 설정 JSON 안 건드리고 패널에서 관리. 타입체크·빌드 통과. Co-Authored-By: Claude Opus 4.8 --- media/settings-panel.html | 40 ++++++++ media/settings-panel.js | 41 +++++++++ .../settings/settingsPanelProvider.ts | 92 +++++++++++++++++++ 3 files changed, 173 insertions(+) diff --git a/media/settings-panel.html b/media/settings-panel.html index 0bf76e0..3eb17c6 100644 --- a/media/settings-panel.html +++ b/media/settings-panel.html @@ -117,6 +117,46 @@ + +
+

이메일 (Project Astra)

+

Gmail 을 읽기 전용으로 수집해 로컬 인덱스에 저장하고, 채팅이 메일 근거(원문 링크 포함)로 답하게 합니다. 본문은 로컬을 벗어나지 않으며 합성은 로컬 LLM 만 사용합니다. (최초 1회 gmail.readonly 재인증 필요)

+
+ + +
+
+ + + 켜면 백그라운드에서 주기적으로 자동 수집합니다. +
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ + +
+
+

메모리

diff --git a/media/settings-panel.js b/media/settings-panel.js index 5196c03..5b7f8ac 100644 --- a/media/settings-panel.js +++ b/media/settings-panel.js @@ -34,6 +34,15 @@ const dcMaxPages = $('dcMaxPages'); const dcSynthTemp = $('dcSynthTemp'); + // ---- Email (Project Astra) ---- + const emailStatus = $('emailStatus'); + const emailAutoSync = $('emailAutoSync'); + const emailInterval = $('emailInterval'); + const emailDays = $('emailDays'); + const emailMax = $('emailMax'); + const emailSyncNow = $('emailSyncNow'); + const emailSyncMsg = $('emailSyncMsg'); + // ---- Memory ---- const memEnabled = $('memEnabled'); const memShort = $('memShort'); @@ -153,6 +162,23 @@ vscode.postMessage({ type: 'datacollect.update', synthesisTemperature: Number(dcSynthTemp.value) }) ); + // ---- Email listeners ---- + emailAutoSync.addEventListener('change', () => + vscode.postMessage({ type: 'email.update', autoSync: emailAutoSync.checked }) + ); + document.querySelector('[data-save="email.interval"]').addEventListener('click', () => + vscode.postMessage({ type: 'email.update', autoSyncIntervalMinutes: Number(emailInterval.value) }) + ); + document.querySelector('[data-save="email.days"]').addEventListener('click', () => + vscode.postMessage({ type: 'email.update', syncDays: Number(emailDays.value) }) + ); + document.querySelector('[data-save="email.max"]').addEventListener('click', () => + vscode.postMessage({ type: 'email.update', syncMaxMessages: Number(emailMax.value) }) + ); + emailSyncNow.addEventListener('click', () => + vscode.postMessage({ type: 'email.syncNow' }) + ); + // ---- Memory listeners ---- memEnabled.addEventListener('change', (e) => vscode.postMessage({ type: 'memory.update', memoryEnabled: e.target.checked }) @@ -409,6 +435,21 @@ setIfNotFocused(dcSynthTemp, dc.synthesisTemperature); } + // ---- Email (Project Astra) ---- + const em = state.email; + if (em) { + if (document.activeElement !== emailAutoSync) emailAutoSync.checked = !!em.autoSync; + setIfNotFocused(emailInterval, em.autoSyncIntervalMinutes); + setIfNotFocused(emailDays, em.syncDays); + setIfNotFocused(emailMax, em.syncMaxMessages); + emailStatus.textContent = em.indexedCount > 0 + ? `${em.indexedCount}건 저장됨${em.newestDate ? ` · 최신 ${em.newestDate}` : ''}` + : '수집된 메일 없음 — "지금 동기화" 또는 /email-sync 실행'; + emailSyncMsg.textContent = em.lastSyncMessage || ''; + emailSyncNow.disabled = !!em.syncing; + emailSyncNow.textContent = em.syncing ? '동기화 중…' : '지금 동기화'; + } + // ---- Memory ---- const mem = state.memory; memEnabled.checked = !!mem.memoryEnabled; diff --git a/src/features/settings/settingsPanelProvider.ts b/src/features/settings/settingsPanelProvider.ts index 8d402d4..5ceaee5 100644 --- a/src/features/settings/settingsPanelProvider.ts +++ b/src/features/settings/settingsPanelProvider.ts @@ -6,6 +6,9 @@ import type { TelegramBot } from '../../integrations/telegram/telegramBot'; import { logError, logInfo } from '../../utils'; import { discoverModels } from '../../lib/discoverModels'; import { pickConfigTarget } from '../../lib/paths'; +import { getConfig } from '../../config'; +import { loadEmailRecords } from '../email/emailStore'; +import { syncEmails } from '../email/emailSync'; /** * Astra Settings webview. @@ -101,6 +104,20 @@ interface SettingsState { maxPages: number; synthesisTemperature: number; }; + email: { + autoSync: boolean; + autoSyncIntervalMinutes: number; + syncDays: number; + syncMaxMessages: number; + /** 로컬 인덱스에 저장된 메일 수. */ + indexedCount: number; + /** 가장 최근 메일 날짜(YYYY-MM-DD) — 없으면 ''. */ + newestDate: string; + /** 마지막 '지금 동기화' 결과 메시지. */ + lastSyncMessage: string; + /** 동기화 진행 중. */ + syncing: boolean; + }; google: { clientId: string; /** secret 자체는 client 에 echo 안 함 — *설정 여부* 만. true 면 input placeholder 가 "저장됨" 으로 바뀜. */ @@ -148,6 +165,10 @@ export class SettingsPanelProvider implements vscode.WebviewViewProvider { constructor(private readonly _deps: SettingsPanelDeps) {} + // Project Astra — 이메일 '지금 동기화' 상태(패널 표시용). + private _emailSyncing = false; + private _emailLastSyncMsg = ''; + public resolveWebviewView(view: vscode.WebviewView): void { this._view = view; this._setupWebview(view.webview); @@ -258,6 +279,12 @@ export class SettingsPanelProvider implements vscode.WebviewViewProvider { case 'datacollect.update': await this._handleDatacollectUpdate(msg); return; + case 'email.update': + await this._handleEmailUpdate(msg); + return; + case 'email.syncNow': + await this._handleEmailSyncNow(); + return; case 'google.update': await this._handleGoogleUpdate(msg); return; @@ -638,6 +665,70 @@ export class SettingsPanelProvider implements vscode.WebviewViewProvider { } } + // ────────────── Email (Project Astra) ────────────── + + private async _handleEmailUpdate(msg: any): Promise { + if (typeof msg.autoSync === 'boolean') { + await this._safeConfigUpdate('email.autoSync', msg.autoSync); + } + if (typeof msg.autoSyncIntervalMinutes === 'number' && Number.isFinite(msg.autoSyncIntervalMinutes)) { + await this._safeConfigUpdate('email.autoSyncIntervalMinutes', Math.max(5, Math.min(1440, Math.floor(msg.autoSyncIntervalMinutes)))); + } + if (typeof msg.syncDays === 'number' && Number.isFinite(msg.syncDays)) { + await this._safeConfigUpdate('email.syncDays', Math.max(1, Math.min(365, Math.floor(msg.syncDays)))); + } + if (typeof msg.syncMaxMessages === 'number' && Number.isFinite(msg.syncMaxMessages)) { + await this._safeConfigUpdate('email.syncMaxMessages', Math.max(1, Math.min(2000, Math.floor(msg.syncMaxMessages)))); + } + } + + /** 패널의 '지금 동기화' 버튼 — 슬래시 명령과 동일한 syncEmails 코어 호출. */ + private async _handleEmailSyncNow(): Promise { + if (this._emailSyncing) return; + const c = vscode.workspace.getConfiguration('g1nation'); + const days = c.get('email.syncDays', 7) ?? 7; + const maxResults = Math.max(1, Math.min(2000, c.get('email.syncMaxMessages', 200) ?? 200)); + this._emailSyncing = true; + this._emailLastSyncMsg = '동기화 중…'; + await this._refreshState(); + try { + const r = await syncEmails(this._deps.context, { days, maxResults }); + this._emailLastSyncMsg = r.ok + ? `완료 — 신규 ${r.added} / 총 ${r.total}${r.embedded ? ` · 임베딩 ${r.embedded}` : ''}${r.failed ? ` · 실패 ${r.failed}` : ''}` + : `실패 — ${r.error}`; + } catch (e: any) { + this._emailLastSyncMsg = `오류 — ${e?.message || String(e)}`; + } finally { + this._emailSyncing = false; + await this._refreshState(); + } + } + + private _buildEmailState(): SettingsState['email'] { + const c = vscode.workspace.getConfiguration('g1nation'); + let indexedCount = 0; + let newestDate = ''; + try { + const brainPath = (getConfig().localBrainPath || '').trim(); + if (brainPath) { + const records = loadEmailRecords(brainPath); + indexedCount = records.length; + const newest = records.reduce((m, r) => (r.date > m ? r.date : m), 0); + if (newest > 0) newestDate = new Date(newest).toISOString().slice(0, 10); + } + } catch { /* status 표시 실패는 무해 */ } + return { + autoSync: c.get('email.autoSync', false), + autoSyncIntervalMinutes: c.get('email.autoSyncIntervalMinutes', 30) ?? 30, + syncDays: c.get('email.syncDays', 7) ?? 7, + syncMaxMessages: c.get('email.syncMaxMessages', 200) ?? 200, + indexedCount, + newestDate, + lastSyncMessage: this._emailLastSyncMsg, + syncing: this._emailSyncing, + }; + } + private async _refreshState(): Promise { if (!this._view && !this._panel) return; const cfg = vscode.workspace.getConfiguration('g1nation'); @@ -700,6 +791,7 @@ export class SettingsPanelProvider implements vscode.WebviewViewProvider { maxPages: cfg.get('datacollectMaxPages', 8) ?? 8, synthesisTemperature: cfg.get('datacollectSynthesisTemperature', 0.1) ?? 0.1, }, + email: this._buildEmailState(), google: this._buildGoogleState(), providers: await this._buildProvidersState(), devilAgent: { enabled: cfg.get('devilAgent.enabled', false) },