feat(astra): 이메일 Settings 패널 섹션 (자동동기화 토글 + 지금 동기화 + 인덱스 상태)
Astra Settings 패널에 Email 섹션 추가 — autoSync 토글, 간격/범위/최대수 설정, '지금 동기화' 버튼(슬래시와 동일 syncEmails 코어), 인덱스 상태(건수/최신일) 표시. VSCode 설정 JSON 안 건드리고 패널에서 관리. 타입체크·빌드 통과. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -117,6 +117,46 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- Email (Project Astra) -->
|
||||||
|
<section class="section" data-section="email">
|
||||||
|
<h2>이메일 (Project Astra)</h2>
|
||||||
|
<p class="hint">Gmail 을 <strong>읽기 전용</strong>으로 수집해 로컬 인덱스에 저장하고, 채팅이 메일 근거(원문 링크 포함)로 답하게 합니다. 본문은 로컬을 벗어나지 않으며 합성은 로컬 LLM 만 사용합니다. (최초 1회 <code>gmail.readonly</code> 재인증 필요)</p>
|
||||||
|
<div class="row">
|
||||||
|
<label>인덱스 상태</label>
|
||||||
|
<small id="emailStatus" class="hint">—</small>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<label for="emailAutoSync">자동 동기화</label>
|
||||||
|
<input id="emailAutoSync" type="checkbox" />
|
||||||
|
<small class="hint">켜면 백그라운드에서 주기적으로 자동 수집합니다.</small>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<label for="emailInterval">동기화 간격(분)</label>
|
||||||
|
<div class="input-group narrow">
|
||||||
|
<input id="emailInterval" type="number" min="5" max="1440" step="5" />
|
||||||
|
<button data-save="email.interval">저장</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<label for="emailDays">수집 범위(일)</label>
|
||||||
|
<div class="input-group narrow">
|
||||||
|
<input id="emailDays" type="number" min="1" max="365" step="1" />
|
||||||
|
<button data-save="email.days">저장</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<label for="emailMax">최대 메일 수</label>
|
||||||
|
<div class="input-group narrow">
|
||||||
|
<input id="emailMax" type="number" min="1" max="2000" step="50" />
|
||||||
|
<button data-save="email.max">저장</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<button id="emailSyncNow">지금 동기화</button>
|
||||||
|
<small id="emailSyncMsg" class="hint"></small>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- Memory -->
|
<!-- Memory -->
|
||||||
<section class="section" data-section="memory">
|
<section class="section" data-section="memory">
|
||||||
<h2>메모리</h2>
|
<h2>메모리</h2>
|
||||||
|
|||||||
@@ -34,6 +34,15 @@
|
|||||||
const dcMaxPages = $('dcMaxPages');
|
const dcMaxPages = $('dcMaxPages');
|
||||||
const dcSynthTemp = $('dcSynthTemp');
|
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 ----
|
// ---- Memory ----
|
||||||
const memEnabled = $('memEnabled');
|
const memEnabled = $('memEnabled');
|
||||||
const memShort = $('memShort');
|
const memShort = $('memShort');
|
||||||
@@ -153,6 +162,23 @@
|
|||||||
vscode.postMessage({ type: 'datacollect.update', synthesisTemperature: Number(dcSynthTemp.value) })
|
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 ----
|
// ---- Memory listeners ----
|
||||||
memEnabled.addEventListener('change', (e) =>
|
memEnabled.addEventListener('change', (e) =>
|
||||||
vscode.postMessage({ type: 'memory.update', memoryEnabled: e.target.checked })
|
vscode.postMessage({ type: 'memory.update', memoryEnabled: e.target.checked })
|
||||||
@@ -409,6 +435,21 @@
|
|||||||
setIfNotFocused(dcSynthTemp, dc.synthesisTemperature);
|
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 ----
|
// ---- Memory ----
|
||||||
const mem = state.memory;
|
const mem = state.memory;
|
||||||
memEnabled.checked = !!mem.memoryEnabled;
|
memEnabled.checked = !!mem.memoryEnabled;
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ import type { TelegramBot } from '../../integrations/telegram/telegramBot';
|
|||||||
import { logError, logInfo } from '../../utils';
|
import { logError, logInfo } from '../../utils';
|
||||||
import { discoverModels } from '../../lib/discoverModels';
|
import { discoverModels } from '../../lib/discoverModels';
|
||||||
import { pickConfigTarget } from '../../lib/paths';
|
import { pickConfigTarget } from '../../lib/paths';
|
||||||
|
import { getConfig } from '../../config';
|
||||||
|
import { loadEmailRecords } from '../email/emailStore';
|
||||||
|
import { syncEmails } from '../email/emailSync';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Astra Settings webview.
|
* Astra Settings webview.
|
||||||
@@ -101,6 +104,20 @@ interface SettingsState {
|
|||||||
maxPages: number;
|
maxPages: number;
|
||||||
synthesisTemperature: number;
|
synthesisTemperature: number;
|
||||||
};
|
};
|
||||||
|
email: {
|
||||||
|
autoSync: boolean;
|
||||||
|
autoSyncIntervalMinutes: number;
|
||||||
|
syncDays: number;
|
||||||
|
syncMaxMessages: number;
|
||||||
|
/** 로컬 인덱스에 저장된 메일 수. */
|
||||||
|
indexedCount: number;
|
||||||
|
/** 가장 최근 메일 날짜(YYYY-MM-DD) — 없으면 ''. */
|
||||||
|
newestDate: string;
|
||||||
|
/** 마지막 '지금 동기화' 결과 메시지. */
|
||||||
|
lastSyncMessage: string;
|
||||||
|
/** 동기화 진행 중. */
|
||||||
|
syncing: boolean;
|
||||||
|
};
|
||||||
google: {
|
google: {
|
||||||
clientId: string;
|
clientId: string;
|
||||||
/** secret 자체는 client 에 echo 안 함 — *설정 여부* 만. true 면 input placeholder 가 "저장됨" 으로 바뀜. */
|
/** secret 자체는 client 에 echo 안 함 — *설정 여부* 만. true 면 input placeholder 가 "저장됨" 으로 바뀜. */
|
||||||
@@ -148,6 +165,10 @@ export class SettingsPanelProvider implements vscode.WebviewViewProvider {
|
|||||||
|
|
||||||
constructor(private readonly _deps: SettingsPanelDeps) {}
|
constructor(private readonly _deps: SettingsPanelDeps) {}
|
||||||
|
|
||||||
|
// Project Astra — 이메일 '지금 동기화' 상태(패널 표시용).
|
||||||
|
private _emailSyncing = false;
|
||||||
|
private _emailLastSyncMsg = '';
|
||||||
|
|
||||||
public resolveWebviewView(view: vscode.WebviewView): void {
|
public resolveWebviewView(view: vscode.WebviewView): void {
|
||||||
this._view = view;
|
this._view = view;
|
||||||
this._setupWebview(view.webview);
|
this._setupWebview(view.webview);
|
||||||
@@ -258,6 +279,12 @@ export class SettingsPanelProvider implements vscode.WebviewViewProvider {
|
|||||||
case 'datacollect.update':
|
case 'datacollect.update':
|
||||||
await this._handleDatacollectUpdate(msg);
|
await this._handleDatacollectUpdate(msg);
|
||||||
return;
|
return;
|
||||||
|
case 'email.update':
|
||||||
|
await this._handleEmailUpdate(msg);
|
||||||
|
return;
|
||||||
|
case 'email.syncNow':
|
||||||
|
await this._handleEmailSyncNow();
|
||||||
|
return;
|
||||||
case 'google.update':
|
case 'google.update':
|
||||||
await this._handleGoogleUpdate(msg);
|
await this._handleGoogleUpdate(msg);
|
||||||
return;
|
return;
|
||||||
@@ -638,6 +665,70 @@ export class SettingsPanelProvider implements vscode.WebviewViewProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ────────────── Email (Project Astra) ──────────────
|
||||||
|
|
||||||
|
private async _handleEmailUpdate(msg: any): Promise<void> {
|
||||||
|
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<void> {
|
||||||
|
if (this._emailSyncing) return;
|
||||||
|
const c = vscode.workspace.getConfiguration('g1nation');
|
||||||
|
const days = c.get<number>('email.syncDays', 7) ?? 7;
|
||||||
|
const maxResults = Math.max(1, Math.min(2000, c.get<number>('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<boolean>('email.autoSync', false),
|
||||||
|
autoSyncIntervalMinutes: c.get<number>('email.autoSyncIntervalMinutes', 30) ?? 30,
|
||||||
|
syncDays: c.get<number>('email.syncDays', 7) ?? 7,
|
||||||
|
syncMaxMessages: c.get<number>('email.syncMaxMessages', 200) ?? 200,
|
||||||
|
indexedCount,
|
||||||
|
newestDate,
|
||||||
|
lastSyncMessage: this._emailLastSyncMsg,
|
||||||
|
syncing: this._emailSyncing,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private async _refreshState(): Promise<void> {
|
private async _refreshState(): Promise<void> {
|
||||||
if (!this._view && !this._panel) return;
|
if (!this._view && !this._panel) return;
|
||||||
const cfg = vscode.workspace.getConfiguration('g1nation');
|
const cfg = vscode.workspace.getConfiguration('g1nation');
|
||||||
@@ -700,6 +791,7 @@ export class SettingsPanelProvider implements vscode.WebviewViewProvider {
|
|||||||
maxPages: cfg.get<number>('datacollectMaxPages', 8) ?? 8,
|
maxPages: cfg.get<number>('datacollectMaxPages', 8) ?? 8,
|
||||||
synthesisTemperature: cfg.get<number>('datacollectSynthesisTemperature', 0.1) ?? 0.1,
|
synthesisTemperature: cfg.get<number>('datacollectSynthesisTemperature', 0.1) ?? 0.1,
|
||||||
},
|
},
|
||||||
|
email: this._buildEmailState(),
|
||||||
google: this._buildGoogleState(),
|
google: this._buildGoogleState(),
|
||||||
providers: await this._buildProvidersState(),
|
providers: await this._buildProvidersState(),
|
||||||
devilAgent: { enabled: cfg.get<boolean>('devilAgent.enabled', false) },
|
devilAgent: { enabled: cfg.get<boolean>('devilAgent.enabled', false) },
|
||||||
|
|||||||
Reference in New Issue
Block a user