v2.2.268-291: /email 통합(브리핑·스캔·워처) + stocks 자동편입·매수알림 + 지식문서·메신저 중앙화
[/email — Gmail + 클래식 Outlook (신규 기능군)]
- 기본 명령: brief(PM 업무 허브 대시보드)·list·read·wikify·reply(Drafts만)·open(Outlook 창 열기)
- Gmail: 자체 OAuth gmail.readonly 스코프 추가 (재연결 1회 필요) / Outlook: COM(PowerShell), 전 계정(Store)·하위 폴더(120개 가드) 수집, To/CC 판별
- 브리핑: 상태 3분류(결정/회신/확인만)·P등급 영향·프로젝트 위험도·Today Focus·오늘 완료 예상·기간 명시 시 표시 범위 제한
- 영속 상태(email_state.json): 회신대기/공넘김 상태머신, 창 밖 미회신 보존, done old N 일괄정리
- 워처: 주기 자동 수집 + 미회신 임계 알림(기본 끔) + 데일리 브리핑 '답할 이메일' 섹션
- 전체 스캔(/email scan): 자동 위키화(회차 15건·레지스트리·파일삭제 감지 재생성)·진행 표시(streamProgress)
- 자동발신 필터 확장(Confluence·AWS·광고·[Notification]) + 라벨 오타 후처리(polishBriefLabels)
[/stocks]
- discover 자동 편입(stocks.json) + 매수권장가 224일선 자동 산출
- 매수사정권 진입 전이 감지 → 텔레그램 1회 알림 (직전신호 추적)
- 버그 수정: '미충족'이 .includes('충족')에 오탐되던 필터 판정
[지식 문서 파이프라인 통일 (wikiFormat/wikiSave)]
- PC 로컬 직접 저장 (NAS 볼륨 미아 문제 해소) — 설정 폴더 → 두뇌 → 워크스페이스
- 이메일 전용 Email_Wiki 분리(email.wikifySavePath) + 봉투(frontmatter) 자동 통일
- 정본 프롬프트 단일 빌더(buildKnowledgeWikiPrompt) — web/email 래퍼화, 템플릿 fetch 일원화
- 이메일 문서 시점 3중 방어: 기준시점 헤더·제목=메일 최신일·날짜 없는 현재형 금지
- docs/WIKI_DOC_PIPELINE.md 개발 규약
[텔레그램 메신저 중앙화 (messageFormat)]
- 단일 렌더러 renderTelegram — 5개 발신 빌더(주식보고·매수시그널·Top5·데일리·미회신) 통일
- 주식 보고 시각 하드코딩 제거(messenger.stocksReportTimes CSV) + 알림 토글 2종
- 설정 패널 '메신저' 탭 신설 · '이메일' 토글 섹션(Gmail/Outlook on/off)
테스트 803개 통과 (이메일 상태머신·CC판별·포맷·매수전이·봉투 등 60+ 신규)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,7 @@
|
||||
<button type="button" class="tab" data-tab="model">모델·연결</button>
|
||||
<button type="button" class="tab" data-tab="knowledge">지식·기억</button>
|
||||
<button type="button" class="tab" data-tab="integrations">연동</button>
|
||||
<button type="button" class="tab" data-tab="messenger">메신저</button>
|
||||
<button type="button" class="tab" data-tab="advanced">동작·고급</button>
|
||||
</nav>
|
||||
|
||||
@@ -90,14 +91,6 @@
|
||||
</div>
|
||||
<small class="hint">NAS Bridge의 <code>x-bridge-token</code>. <strong>nas</strong> 타깃일 때만 요청 헤더에 실립니다.</small>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label for="dcSavePath">결과물 저장 폴더</label>
|
||||
<div class="input-group">
|
||||
<input id="dcSavePath" type="text" placeholder="(비우면 Bridge 기본 위치)" spellcheck="false" />
|
||||
<button data-save="datacollect.savePath">저장</button>
|
||||
</div>
|
||||
<small class="hint">/benchmark 등의 결과 markdown 저장 위치. 비워두면 Bridge의 <code>WIKI_RAW_PATH</code> 환경변수가 결정합니다 (코드에 절대경로 하드코딩 없음). 특정 폴더로 저장하려면 절대경로를 입력하세요.</small>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label for="dcCrawlDepth">크롤 깊이 기본값</label>
|
||||
<div class="input-group narrow">
|
||||
@@ -169,6 +162,81 @@
|
||||
<div class="row">
|
||||
<button id="brainOpenSettings" class="link">brainProfiles 편집 (VS Code Settings)</button>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label for="dcSavePath">지식 문서 저장 폴더</label>
|
||||
<div class="input-group">
|
||||
<input id="dcSavePath" type="text" placeholder="(비우면 활성 두뇌의 Datacollect_Wiki)" spellcheck="false" />
|
||||
<button data-save="datacollect.savePath">저장</button>
|
||||
</div>
|
||||
<small class="hint"><code>/wikify</code> · <code>/meet</code> · <code>/benchmark</code> · <code>/youtube</code> · <code>/review</code> 가 생성하는 지식 문서(markdown)의 저장 위치. <strong>PC 로컬에 직접 저장</strong>되며, 비워두면 활성 두뇌 폴더의 <code>Datacollect_Wiki/</code> 에 저장돼 brain 검색에 자동 포함됩니다. 다른 곳에 두려면 절대경로 입력 (예: <code>E:\Wiki\2nd\10_Wiki</code>).</small>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label for="emailWikiSavePath">이메일 지식 문서 저장 폴더</label>
|
||||
<div class="input-group">
|
||||
<input id="emailWikiSavePath" type="text" placeholder="(비우면 활성 두뇌의 Email_Wiki)" spellcheck="false" />
|
||||
<button data-save="email.wikifySavePath">저장</button>
|
||||
</div>
|
||||
<small class="hint"><code>/email wikify</code> 로 이메일 스레드를 지식화한 문서는 다른 wikify 산출물과 <strong>별도 폴더에서 관리</strong>됩니다. 비워두면 활성 두뇌 폴더의 <code>Email_Wiki/</code> 에 저장. 다른 곳에 두려면 절대경로 입력.</small>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Messenger — 텔레그램 발송 스케줄·알림 (v2.2.284 중앙화) -->
|
||||
<section class="section" data-section="messenger" data-tab="messenger">
|
||||
<h2>발송 스케줄</h2>
|
||||
<p class="hint">텔레그램으로 나가는 정기 메시지의 시각을 여기서 관리합니다 (KST 기준).</p>
|
||||
<div class="row">
|
||||
<label for="msgStocksTimes">주식 정기 보고서 시각</label>
|
||||
<div class="input-group">
|
||||
<input id="msgStocksTimes" type="text" placeholder="09:00,15:00 (비우면 정기 실행 없음)" spellcheck="false" />
|
||||
<button data-save="messenger.stocksReportTimes">저장</button>
|
||||
</div>
|
||||
<small class="hint">HH:MM 을 쉼표로 구분 (예: <code>09:00,12:30,15:00</code>). 이 시각마다 가격 갱신·매수시그널 감지·보고서 발송.</small>
|
||||
</div>
|
||||
<div class="row toggle">
|
||||
<label><input id="msgBriefingEnabled" type="checkbox"> 데일리 브리핑 (일정·할 일·답할 이메일)</label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label for="msgBriefingTime">데일리 브리핑 시각</label>
|
||||
<div class="input-group narrow">
|
||||
<input id="msgBriefingTime" type="text" placeholder="09:30" spellcheck="false" />
|
||||
<button data-save="messenger.briefingTime">저장</button>
|
||||
</div>
|
||||
<small class="hint">평일(월~금) 이 시각에 1회 발송. 미회신 이메일 정리는 이 메시지에 포함됩니다.</small>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section" data-section="messengerAlerts" data-tab="messenger">
|
||||
<h2>이벤트 알림</h2>
|
||||
<p class="hint">상황이 발생했을 때 즉시 발송되는 알림들입니다.</p>
|
||||
<div class="row toggle">
|
||||
<label><input id="msgBuySignal" type="checkbox"> 🚨 매수 시그널 진입 알림</label>
|
||||
<small class="hint">관망 종목이 매수사정권에 진입하는 순간 1회.</small>
|
||||
</div>
|
||||
<div class="row toggle">
|
||||
<label><input id="msgDiscoverTop5" type="checkbox"> 🎯 발굴 Top 5 (/stocks discover 후)</label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label for="msgEmailOverdue">📧 이메일 미회신 즉시 알림 임계(시간)</label>
|
||||
<div class="input-group narrow">
|
||||
<input id="msgEmailOverdue" type="number" min="0" step="1" />
|
||||
<button data-save="messenger.emailOverdueHours">저장</button>
|
||||
</div>
|
||||
<small class="hint"><code>0</code> = 끔(기본) — 미회신 정리는 데일리 브리핑이 담당. 즉시 알림을 원하면 시간 입력 (예: 24).</small>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Email providers (/email) -->
|
||||
<section class="section" data-section="email" data-tab="integrations">
|
||||
<h2>이메일 (/email)</h2>
|
||||
<p class="hint">끈 프로바이더는 수집 시도 자체를 하지 않아 채팅에 "제외/실패" 문구가 뜨지 않습니다.</p>
|
||||
<div class="row toggle">
|
||||
<label><input id="emailOutlookEnabled" type="checkbox"> Outlook(클래식) 사용</label>
|
||||
<small class="hint">데스크톱 클래식 Outlook COM — 별도 인증 없음. "새 Outlook" 토글 ON 상태는 미지원.</small>
|
||||
</div>
|
||||
<div class="row toggle">
|
||||
<label><input id="emailGmailEnabled" type="checkbox"> Gmail 사용</label>
|
||||
<small class="hint">Google OAuth 재연결 1회 필요 (gmail.readonly 동의). 연결 전이거나 안 쓰면 꺼두세요 — /email 출력이 깔끔해집니다.</small>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Telegram -->
|
||||
|
||||
@@ -30,6 +30,15 @@
|
||||
const dcBridgeNasUrl = $('dcBridgeNasUrl');
|
||||
const dcBridgeNasToken = $('dcBridgeNasToken');
|
||||
const dcSavePath = $('dcSavePath');
|
||||
const emailWikiSavePath = $('emailWikiSavePath');
|
||||
const emailOutlookEnabled = $('emailOutlookEnabled');
|
||||
const emailGmailEnabled = $('emailGmailEnabled');
|
||||
const msgStocksTimes = $('msgStocksTimes');
|
||||
const msgBriefingEnabled = $('msgBriefingEnabled');
|
||||
const msgBriefingTime = $('msgBriefingTime');
|
||||
const msgBuySignal = $('msgBuySignal');
|
||||
const msgDiscoverTop5 = $('msgDiscoverTop5');
|
||||
const msgEmailOverdue = $('msgEmailOverdue');
|
||||
const dcCrawlDepth = $('dcCrawlDepth');
|
||||
const dcMaxPages = $('dcMaxPages');
|
||||
const dcSynthTemp = $('dcSynthTemp');
|
||||
@@ -143,6 +152,9 @@
|
||||
document.querySelector('[data-save="datacollect.savePath"]').addEventListener('click', () =>
|
||||
vscode.postMessage({ type: 'datacollect.update', savePath: dcSavePath.value })
|
||||
);
|
||||
document.querySelector('[data-save="email.wikifySavePath"]').addEventListener('click', () =>
|
||||
vscode.postMessage({ type: 'datacollect.update', emailWikifySavePath: emailWikiSavePath.value })
|
||||
);
|
||||
document.querySelector('[data-save="datacollect.crawlDepth"]').addEventListener('click', () =>
|
||||
vscode.postMessage({ type: 'datacollect.update', crawlDepth: Number(dcCrawlDepth.value) })
|
||||
);
|
||||
@@ -171,6 +183,30 @@
|
||||
brainAutoPush.addEventListener('change', (e) =>
|
||||
vscode.postMessage({ type: 'brain.update', autoPushBrain: e.target.checked })
|
||||
);
|
||||
emailOutlookEnabled.addEventListener('change', (e) =>
|
||||
vscode.postMessage({ type: 'datacollect.update', emailOutlookEnabled: e.target.checked })
|
||||
);
|
||||
emailGmailEnabled.addEventListener('change', (e) =>
|
||||
vscode.postMessage({ type: 'datacollect.update', emailGmailEnabled: e.target.checked })
|
||||
);
|
||||
document.querySelector('[data-save="messenger.stocksReportTimes"]').addEventListener('click', () =>
|
||||
vscode.postMessage({ type: 'messenger.update', stocksReportTimes: msgStocksTimes.value })
|
||||
);
|
||||
msgBriefingEnabled.addEventListener('change', (e) =>
|
||||
vscode.postMessage({ type: 'messenger.update', briefingEnabled: e.target.checked })
|
||||
);
|
||||
document.querySelector('[data-save="messenger.briefingTime"]').addEventListener('click', () =>
|
||||
vscode.postMessage({ type: 'messenger.update', briefingTime: msgBriefingTime.value })
|
||||
);
|
||||
msgBuySignal.addEventListener('change', (e) =>
|
||||
vscode.postMessage({ type: 'messenger.update', buySignalAlert: e.target.checked })
|
||||
);
|
||||
msgDiscoverTop5.addEventListener('change', (e) =>
|
||||
vscode.postMessage({ type: 'messenger.update', discoverTopFive: e.target.checked })
|
||||
);
|
||||
document.querySelector('[data-save="messenger.emailOverdueHours"]').addEventListener('click', () =>
|
||||
vscode.postMessage({ type: 'messenger.update', emailOverdueHours: Number(msgEmailOverdue.value) })
|
||||
);
|
||||
$('brainOpenSettings').addEventListener('click', () =>
|
||||
vscode.postMessage({ type: 'openVscodeSettings' })
|
||||
);
|
||||
@@ -404,6 +440,19 @@
|
||||
setIfNotFocused(dcBridgeNasUrl, dc.bridgeNasUrl);
|
||||
setIfNotFocused(dcBridgeNasToken, dc.bridgeNasToken);
|
||||
setIfNotFocused(dcSavePath, dc.savePath);
|
||||
setIfNotFocused(emailWikiSavePath, dc.emailWikifySavePath);
|
||||
if (state.messenger) {
|
||||
setIfNotFocused(msgStocksTimes, state.messenger.stocksReportTimes);
|
||||
msgBriefingEnabled.checked = !!state.messenger.briefingEnabled;
|
||||
setIfNotFocused(msgBriefingTime, state.messenger.briefingTime);
|
||||
msgBuySignal.checked = !!state.messenger.buySignalAlert;
|
||||
msgDiscoverTop5.checked = !!state.messenger.discoverTopFive;
|
||||
setIfNotFocused(msgEmailOverdue, state.messenger.emailOverdueHours);
|
||||
}
|
||||
if (state.email) {
|
||||
emailOutlookEnabled.checked = !!state.email.outlookEnabled;
|
||||
emailGmailEnabled.checked = !!state.email.gmailEnabled;
|
||||
}
|
||||
setIfNotFocused(dcCrawlDepth, dc.crawlDepth);
|
||||
setIfNotFocused(dcMaxPages, dc.maxPages);
|
||||
setIfNotFocused(dcSynthTemp, dc.synthesisTemperature);
|
||||
|
||||
@@ -838,11 +838,27 @@
|
||||
break;
|
||||
case 'streamChunk':
|
||||
if (streamBody) {
|
||||
// 진행 표시(streamProgress)가 떠 있으면 걷어내고 본문을 잇는다.
|
||||
if (streamBody._parent._progressBase !== undefined) {
|
||||
streamBody._parent._raw = streamBody._parent._progressBase;
|
||||
streamBody._parent._progressBase = undefined;
|
||||
}
|
||||
streamBody._parent._raw += msg.value;
|
||||
streamBody.innerHTML = fmt(streamBody._parent._raw);
|
||||
chat.scrollTop = chat.scrollHeight;
|
||||
}
|
||||
break;
|
||||
case 'streamProgress': {
|
||||
// 제자리 갱신 진행 표시 — 직전 progress 줄을 교체한다 (로그 누적 방지).
|
||||
if (!streamBody) break;
|
||||
if (streamBody._parent._progressBase === undefined) {
|
||||
streamBody._parent._progressBase = streamBody._parent._raw;
|
||||
}
|
||||
streamBody._parent._raw = streamBody._parent._progressBase + String(msg.value ?? '');
|
||||
streamBody.innerHTML = fmt(streamBody._parent._raw);
|
||||
chat.scrollTop = chat.scrollHeight;
|
||||
break;
|
||||
}
|
||||
case 'streamReplace':
|
||||
// Progressive answering: the backend streamed raw tokens
|
||||
// live (including hidden reasoning, pre-sanitize text);
|
||||
@@ -850,6 +866,7 @@
|
||||
// text via streamReplace so the bubble ends up correct
|
||||
// regardless of what slipped through during streaming.
|
||||
if (streamBody) {
|
||||
streamBody._parent._progressBase = undefined;
|
||||
streamBody._parent._raw = String(msg.value ?? '');
|
||||
streamBody.innerHTML = fmt(streamBody._parent._raw);
|
||||
chat.scrollTop = chat.scrollHeight;
|
||||
|
||||
Reference in New Issue
Block a user