Files
connectai/src/features/stocks/stocksWatcher.ts
T
g1nation 0a97324f1b feat: v2.2.92 → v2.2.158 — god-file 분해 + Stocks feature + 대화 연속성
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>
2026-05-25 09:59:32 +09:00

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);
}