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>
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { TelegramHttpClient } from '../../integrations/telegram/telegramClient';
|
||||
import { TELEGRAM_TOKEN_SECRET_KEY } from '../../extension/telegramCommands';
|
||||
import { logError, logInfo } from '../../utils';
|
||||
import { readStocksStore } from './stocksStore';
|
||||
import { classifyAll } from './signalClassifier';
|
||||
import type { ClassifiedStock } from './types';
|
||||
|
||||
/**
|
||||
* 09:00 / 15:00 KST 정기 보고서 — Telegram 으로 발송.
|
||||
*
|
||||
* 사용자 결정 (q2=A): chatId 는 `g1nation.telegram.allowedChatIds[0]` 자동 사용.
|
||||
* 없으면 fallback 으로 `g1nation.stocks.telegramChatId` 별도 설정. 둘 다 없으면 skip + 로그.
|
||||
* 토큰은 telegramCommands 의 SecretStorage 키와 공유.
|
||||
*/
|
||||
|
||||
function pickChatId(): number | null {
|
||||
const cfg = vscode.workspace.getConfiguration('g1nation');
|
||||
const allowed = cfg.get<number[]>('telegram.allowedChatIds', []) || [];
|
||||
if (allowed.length > 0) return allowed[0];
|
||||
const dedicated = cfg.get<number>('stocks.telegramChatId', 0);
|
||||
if (dedicated && dedicated !== 0) return dedicated;
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 한 카테고리 (스윙/장기/저평가) 의 종목 리스트를 텔레그램 Markdown 으로 렌더. */
|
||||
function renderGroup(label: string, stocks: ClassifiedStock[]): string[] {
|
||||
const buyZone = stocks.filter(s => s.signal === 'BUY_ZONE');
|
||||
const overvalued = stocks.filter(s => s.signal === 'OVERVALUED');
|
||||
const hold = stocks.filter(s => s.signal === 'HOLD');
|
||||
|
||||
const lines: string[] = [`*${label}* (총 ${stocks.length}개)`];
|
||||
if (buyZone.length > 0) {
|
||||
lines.push(`🚨 매수사정권 (${buyZone.length})`);
|
||||
for (const s of buyZone) {
|
||||
const cur = s.현재가 ? s.현재가.toLocaleString() : '?';
|
||||
lines.push(` · ${s.이름} (${s.심볼}): ${cur} / 권장 ${s.매수권장가 ?? '-'}`);
|
||||
}
|
||||
}
|
||||
if (overvalued.length > 0) {
|
||||
lines.push(`⚠️ 가격 조정 감시 (${overvalued.length})`);
|
||||
for (const s of overvalued) {
|
||||
const cur = s.현재가 ? s.현재가.toLocaleString() : '?';
|
||||
lines.push(` · ${s.이름} (${s.심볼}): ${cur} / 권장 ${s.매수권장가 ?? '-'}`);
|
||||
}
|
||||
}
|
||||
if (hold.length > 0 && buyZone.length === 0 && overvalued.length === 0) {
|
||||
// 모두 관망일 때만 그 카운트만 표시 — 종목 일일이 안 나열 (보고서 길이 절약).
|
||||
lines.push(`📊 관망 ${hold.length}건`);
|
||||
} else if (hold.length > 0) {
|
||||
lines.push(`📊 관망 ${hold.length}건 (생략)`);
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
/** 보고서 텍스트 생성 (전송과 분리 — 테스트·로그용). */
|
||||
export function buildReportText(now: Date = new Date()): string {
|
||||
const store = readStocksStore();
|
||||
if (store.length === 0) {
|
||||
return '⚠️ stocks.json 에 종목 없음 — `/stocks add <심볼> <이름>` 으로 추가하세요.';
|
||||
}
|
||||
const g = classifyAll(store);
|
||||
const kstStr = formatKstTimestamp(now);
|
||||
|
||||
const out: string[] = [`🦅 *Kodari 정기 보고서* _${kstStr}_`, ''];
|
||||
out.push(...renderGroup('스윙/중기', g.swing), '');
|
||||
out.push(...renderGroup('장기투자', g.long), '');
|
||||
out.push(...renderGroup('저평가우량주', g.ultraLow));
|
||||
|
||||
return out.join('\n');
|
||||
}
|
||||
|
||||
/** Date → "2026-05-25 09:00 KST" 형식. */
|
||||
function formatKstTimestamp(d: Date): string {
|
||||
const fmt = new Intl.DateTimeFormat('ko-KR', {
|
||||
timeZone: 'Asia/Seoul',
|
||||
year: 'numeric', month: '2-digit', day: '2-digit',
|
||||
hour: '2-digit', minute: '2-digit', hour12: false,
|
||||
});
|
||||
const parts = fmt.formatToParts(d);
|
||||
const get = (t: string) => parts.find(p => p.type === t)?.value || '';
|
||||
return `${get('year')}-${get('month')}-${get('day')} ${get('hour')}:${get('minute')} KST`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 보고서를 텔레그램으로 전송. 토큰 없거나 chatId 없으면 silent skip (warn 로그).
|
||||
*
|
||||
* 성공/실패 모두 caller (watcher / slash 명령) 가 알 수 있도록 결과 반환.
|
||||
*/
|
||||
export async function sendStocksReport(
|
||||
context: vscode.ExtensionContext,
|
||||
): Promise<{ ok: boolean; reason?: string }> {
|
||||
const chatId = pickChatId();
|
||||
if (chatId === null) {
|
||||
const reason = '텔레그램 chatId 미설정 (g1nation.telegram.allowedChatIds 또는 g1nation.stocks.telegramChatId).';
|
||||
logInfo(`Stocks report skip: ${reason}`);
|
||||
return { ok: false, reason };
|
||||
}
|
||||
|
||||
const token = (await context.secrets.get(TELEGRAM_TOKEN_SECRET_KEY)) || '';
|
||||
if (!token.trim()) {
|
||||
const reason = '텔레그램 봇 토큰 없음 — `Astra: Set Telegram Bot Token` 으로 등록.';
|
||||
logInfo(`Stocks report skip: ${reason}`);
|
||||
return { ok: false, reason };
|
||||
}
|
||||
|
||||
const text = buildReportText();
|
||||
const client = new TelegramHttpClient({ getToken: () => token });
|
||||
try {
|
||||
await client.sendMessage({ chatId, text, parseMode: 'Markdown' });
|
||||
logInfo('Stocks report 발송 완료.', { chatId, chars: text.length });
|
||||
return { ok: true };
|
||||
} catch (e: any) {
|
||||
logError('Stocks report 발송 실패.', { chatId, error: e?.message ?? String(e) });
|
||||
return { ok: false, reason: e?.message ?? String(e) };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user