0a97324f1b
R56–R59: agent.ts 2731→1529줄 god-file 분해 (25 modules) · attrParsers + LLM 메서드 8개 (callNonStreaming, streamChatOnce 등) · executeActions 415줄 → 8 handler 그룹 (file/run/list/brain/calendar/sheets/tasks) · handlePrompt 1100줄 → 7 phase 모듈 (system prompt + budget + autoContinue 등) R50–R55: extension.ts 1145→349줄 (telegram/settings/provider commands 분리) Stocks feature 신규: /stocks slash command (v2.2.152~158) · .astra/stocks.json 저장소 + Yahoo Finance 현재가 갱신 · 8 키워드 필터 (ROE/성장성/유동성/수익성/영업효율/기술력/안정성/PBR) · Naver 시가총액 페이지 JSON API (m.stock.naver.com) 발굴 · LLM Top 5 매력도 분석 + Telegram 자동 보고서 · KST 09:00/15:00 watcher 자동 모니터링 대화 연속성 (v2.2.150~157): · [PRIOR TURN CONCLUSION] block 으로 직전 결론 anchor · thin follow-up 분류 → boilerplate 헤더 suppression · slash 명령 결과 chatHistory mirror (capture wrapper) · echo/parrot 금지 system prompt rule 기타: /stocks 슬래시 자동완성 dropdown UI, Naver JSON API 전환 (cheerio 제거) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
164 lines
6.2 KiB
TypeScript
164 lines
6.2 KiB
TypeScript
import * as vscode from 'vscode';
|
|
import { logError, logInfo } from '../../utils';
|
|
import { readStocksStore, writeStocksStore } from './stocksStore';
|
|
import { fetchAllPrices } from './yahooClient';
|
|
import { sendStocksReport } from './telegramReport';
|
|
import { syncToSheets } from './sheetsSync';
|
|
|
|
/**
|
|
* VS Code 시작 시 자동 활성화 — KST 09:00 / 15:00 에:
|
|
* 1. Yahoo 로 현재가 일괄 갱신 → stocks.json 업데이트
|
|
* 2. (선택) Google Sheets 동기화 — g1nation.stocks.spreadsheetId 설정 시
|
|
* 3. 텔레그램 보고서 발송
|
|
*
|
|
* 구현 노트:
|
|
* - setTimeout 단일 chain — 매 firing 후 다음 알람까지 재계산해서 새 setTimeout.
|
|
* - VS Code 종료 시 disposable 로 clear.
|
|
* - 시간대는 Asia/Seoul 강제 — 사용자 macOS timezone 과 무관하게 같은 시각에 동작.
|
|
* - run_kodari_sync.command 의 sleep-loop 구조를 단일 setTimeout 으로 대체.
|
|
*
|
|
* 트리거 시각 변경 시 SCHEDULE 만 수정 — 분 단위.
|
|
*/
|
|
|
|
const SCHEDULE_HOURS_KST = [9, 15]; // 09:00, 15:00 KST
|
|
|
|
let _timer: NodeJS.Timeout | undefined;
|
|
let _disposed = false;
|
|
|
|
/** Asia/Seoul 기준 *지금* 의 hour/minute. */
|
|
function nowInKst(): { date: Date; hour: number; minute: number; ymd: string } {
|
|
const now = new Date();
|
|
const parts = new Intl.DateTimeFormat('en-US', {
|
|
timeZone: 'Asia/Seoul',
|
|
year: 'numeric', month: '2-digit', day: '2-digit',
|
|
hour: '2-digit', minute: '2-digit', hour12: false,
|
|
}).formatToParts(now);
|
|
const get = (t: string) => Number(parts.find(p => p.type === t)?.value || '0');
|
|
return {
|
|
date: now,
|
|
hour: get('hour'),
|
|
minute: get('minute'),
|
|
ymd: `${get('year')}-${parts.find(p => p.type === 'month')?.value}-${parts.find(p => p.type === 'day')?.value}`,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 지금부터 다음 firing 까지 milliseconds 계산.
|
|
* SCHEDULE_HOURS_KST 중 *오늘 남은 시각* 우선, 다 지났으면 내일 첫 시각.
|
|
*
|
|
* 시간대 변환: 사용자 OS 가 KST 가 아닐 수 있으므로 KST 기준 hour/minute 으로 비교 후
|
|
* 그 차이를 ms 로 변환 (변환은 단순 산수 — 분 차이 × 60000).
|
|
*/
|
|
function msUntilNextRun(): number {
|
|
const kst = nowInKst();
|
|
const todayMinutes = kst.hour * 60 + kst.minute;
|
|
|
|
let bestTodayMinutes: number | null = null;
|
|
for (const h of SCHEDULE_HOURS_KST) {
|
|
const targetMinutes = h * 60;
|
|
if (targetMinutes > todayMinutes) {
|
|
bestTodayMinutes = targetMinutes;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (bestTodayMinutes !== null) {
|
|
return (bestTodayMinutes - todayMinutes) * 60_000;
|
|
}
|
|
// 오늘 더 없음 → 내일 첫 시각.
|
|
const tomorrowFirstMinutes = SCHEDULE_HOURS_KST[0] * 60;
|
|
const remainingTodayMinutes = 24 * 60 - todayMinutes;
|
|
return (remainingTodayMinutes + tomorrowFirstMinutes) * 60_000;
|
|
}
|
|
|
|
/**
|
|
* 한 번 fire — 가격 갱신 + (선택) Sheets sync + 텔레그램 보고서.
|
|
* 각 단계는 독립적으로 try/catch — 한 단계 실패해도 다른 단계는 계속.
|
|
*/
|
|
async function fireOnce(context: vscode.ExtensionContext): Promise<void> {
|
|
const kst = nowInKst();
|
|
logInfo('Stocks watcher fire 시작.', { kst: `${kst.hour}:${kst.minute}` });
|
|
|
|
// (1) Yahoo 가격 갱신
|
|
try {
|
|
const store = readStocksStore();
|
|
const symbols = store.map(s => s.심볼).filter(Boolean);
|
|
if (symbols.length === 0) {
|
|
logInfo('Stocks watcher: 종목 없음 — skip.');
|
|
} else {
|
|
const prices = await fetchAllPrices(symbols);
|
|
for (const s of store) {
|
|
const p = prices.get(s.심볼);
|
|
if (typeof p === 'number') s.현재가 = p;
|
|
}
|
|
writeStocksStore(store);
|
|
}
|
|
} catch (e: any) {
|
|
logError('Stocks watcher: 가격 갱신 실패.', { error: e?.message ?? String(e) });
|
|
}
|
|
|
|
// (2) Sheets sync — 선택 (spreadsheetId 설정 시에만)
|
|
try {
|
|
const cfg = vscode.workspace.getConfiguration('g1nation');
|
|
if ((cfg.get<string>('stocks.spreadsheetId') || '').trim()) {
|
|
const r = await syncToSheets(context);
|
|
if (!r.ok) logError('Stocks watcher: Sheets 동기화 실패.', { errors: r.errors });
|
|
}
|
|
} catch (e: any) {
|
|
logError('Stocks watcher: Sheets 호출 실패.', { error: e?.message ?? String(e) });
|
|
}
|
|
|
|
// (3) Telegram 보고서
|
|
try {
|
|
const r = await sendStocksReport(context);
|
|
if (!r.ok) logInfo(`Stocks watcher: 보고서 skip — ${r.reason}`);
|
|
} catch (e: any) {
|
|
logError('Stocks watcher: 보고서 호출 실패.', { error: e?.message ?? String(e) });
|
|
}
|
|
}
|
|
|
|
function scheduleNext(context: vscode.ExtensionContext): void {
|
|
if (_disposed) return;
|
|
const ms = msUntilNextRun();
|
|
const hours = Math.floor(ms / 3600_000);
|
|
const minutes = Math.floor((ms % 3600_000) / 60_000);
|
|
logInfo(`Stocks watcher: 다음 firing 까지 ${hours}h ${minutes}m.`);
|
|
|
|
_timer = setTimeout(async () => {
|
|
try {
|
|
await fireOnce(context);
|
|
} catch (e: any) {
|
|
logError('Stocks watcher: fireOnce 예외.', { error: e?.message ?? String(e) });
|
|
}
|
|
scheduleNext(context);
|
|
}, ms);
|
|
}
|
|
|
|
/**
|
|
* VS Code 시작 시 호출 (extension.ts activate 의 마지막 단계).
|
|
* 설정 `g1nation.stocks.watcherEnabled` 가 false 이면 활성화 skip.
|
|
*/
|
|
export function startStocksWatcher(context: vscode.ExtensionContext): vscode.Disposable {
|
|
const cfg = vscode.workspace.getConfiguration('g1nation');
|
|
const enabled = cfg.get<boolean>('stocks.watcherEnabled', true);
|
|
if (!enabled) {
|
|
logInfo('Stocks watcher: 비활성 (g1nation.stocks.watcherEnabled=false).');
|
|
return { dispose: () => {} };
|
|
}
|
|
_disposed = false;
|
|
scheduleNext(context);
|
|
logInfo('Stocks watcher: 시작됨.', { schedule: SCHEDULE_HOURS_KST });
|
|
return {
|
|
dispose: () => {
|
|
_disposed = true;
|
|
if (_timer) { clearTimeout(_timer); _timer = undefined; }
|
|
logInfo('Stocks watcher: dispose.');
|
|
},
|
|
};
|
|
}
|
|
|
|
/** 명령으로 즉시 한 번 트리거 (`/stocks watch run` 같은 미래 명령 또는 디버깅). */
|
|
export async function runOnceNow(context: vscode.ExtensionContext): Promise<void> {
|
|
await fireOnce(context);
|
|
}
|