Files
connectai/src/features/stocks/telegramReport.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

118 lines
5.1 KiB
TypeScript

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