Files
connectai/src/features/datacollect/wikiFormat.ts
T
koriweb fa397d7fa1 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>
2026-07-03 18:57:12 +09:00

177 lines
9.0 KiB
TypeScript

/**
* ============================================================
* 위키 문서 포맷 — 단일 정본 모듈 (v2.2.278)
*
* 배경: 위키 문서를 만드는 기능이 늘 때마다(/wikify → /meet → /email → …)
* 포맷이 하나씩 새로 생기는 파편화가 진행됐다. 이 모듈이 그 흐름을 끊는다 —
* **앞으로 위키 문서를 만드는 모든 기능은 여기만 쓴다.**
*
* 2층 구조:
* ① 봉투(frontmatter) 통일 — `ensureFrontmatter()`
* 저장되는 모든 문서에 정본 frontmatter 를 보장한다. LLM 이 정본 규격으로
* 이미 생성한 문서(--- 시작)는 건드리지 않고, 보고서형 문서(/meet 회의록,
* /benchmark, /review, /youtube)에는 결정론 봉투를 씌운다.
* → wikiSave.persistWikiDoc 이 저장 직전 자동 호출 — 호출자는 신경 쓸 것 없음.
* ② 지식 문서 프롬프트 공용 빌더 — `buildKnowledgeWikiPrompt()`
* P-Reinforce 정본 템플릿(frontmatter+섹션)에 소스 종류별 특수 규칙만
* 데이터로 주입한다. 새 소스 추가 = KnowledgeSource 하나 정의가 전부.
*
* 정본 템플릿은 Datacollect 브리지 wiki_format.mjs (getCanonicalFormat 이
* 10분 캐시로 fetch, 실패 시 wikifyPrompt 의 내장 사본 fallback).
* ============================================================
*/
import { bridgeFetch, BRIDGE_API } from './bridgeClient';
import type { CanonicalWikiFormat } from './prompts/wikifyPrompt';
// ── 정본 템플릿 fetch (일원화 + 캐시) ────────────────────────────────────────
let _canonicalCache: { fmt: CanonicalWikiFormat | null; at: number } | undefined;
const CANONICAL_TTL_MS = 10 * 60_000;
/** 브리지의 포맷 정본. 실패 시 null (호출부 빌더가 내장 사본으로 fallback). */
export async function getCanonicalFormat(): Promise<CanonicalWikiFormat | null> {
if (_canonicalCache && Date.now() - _canonicalCache.at < CANONICAL_TTL_MS) return _canonicalCache.fmt;
let fmt: CanonicalWikiFormat | null = null;
try {
fmt = await bridgeFetch<CanonicalWikiFormat>(BRIDGE_API.wiki.template, { method: 'GET' }, { timeoutMs: 5000 });
if (!fmt || !fmt.commonRules || !fmt.frontmatter || !fmt.sections) fmt = null;
} catch { fmt = null; }
_canonicalCache = { fmt, at: Date.now() };
return fmt;
}
// ── ① 봉투(frontmatter) 통일 ─────────────────────────────────────────────────
export interface EnvelopeOpts {
/** 문서 종류 — frontmatter category·tags 로 들어감 (예: 'meeting', 'benchmark', 'review', 'youtube'). */
docType: string;
/** raw_sources 항목 (URL·파일명·"email:제목" 등). */
sources?: string[];
/** 변경 이력 첫 줄 문구. 생략 시 docType 기반 기본 문구. */
origin?: string;
}
/** title → frontmatter id 슬러그 (wikifyPrompt 의 규칙과 동일). */
export function slugifyId(title: string, fallback: string): string {
return (title.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9가-힣-]/g, '').slice(0, 80)) || fallback;
}
/** 문서가 이미 frontmatter(---) 로 시작하는지 — 코드펜스·공백 관대 판정. */
export function hasFrontmatter(content: string): boolean {
const t = (content || '').trimStart();
return t.startsWith('---') || t.replace(/^```(?:markdown|md)?\s*/i, '').trimStart().startsWith('---');
}
/**
* 정본 frontmatter 봉투 보장 — 이미 있으면 원문 그대로, 없으면 결정론 봉투 prepend.
* 봉투 필드는 정본(wiki_format.mjs v3.1) frontmatter 와 같은 이름을 사용해
* 다운스트림(brain 인덱스·그래프·신뢰도 소비자)이 문서 종류와 무관하게 동작한다.
*/
export function ensureFrontmatter(title: string, content: string, opts: EnvelopeOpts): string {
if (hasFrontmatter(content)) return content;
const today = new Date().toISOString().slice(0, 10);
const sources = (opts.sources || []).map(s => JSON.stringify(String(s)));
const fm = [
'---',
`id: ${slugifyId(title, opts.docType + '-doc')}`,
`title: ${JSON.stringify(title)}`,
`category: ${JSON.stringify(opts.docType)}`,
'status: "draft"',
'verification_status: "conceptual"',
'aliases: []',
'source_trust_level: "B"',
'confidence_score: 0.80',
`created_at: ${today}`,
`updated_at: ${today}`,
`tags: ["${opts.docType}", "astra"]`,
`raw_sources: [${sources.join(', ')}]`,
'---',
'',
].join('\n');
return fm + content + `\n\n## 📝 변경 이력 (Change history)\n- ${today}: ${opts.origin || `Astra ${opts.docType} 산출물로 생성.`}\n`;
}
// ── ② 지식 문서 프롬프트 공용 빌더 ───────────────────────────────────────────
/**
* 지식화(위키화) 대상 소스 기술자 — 새 소스 종류는 이 객체 하나로 추가한다.
* (예: 슬랙 스레드 지식화를 만들면 kind:'slack' + 특수 규칙 몇 줄이 전부.)
*/
export interface KnowledgeSource {
/** 소스 종류 라벨 — tags·출처 평가 힌트에 사용. */
kind: string;
/** 문서 주제(제목). */
topic: string;
/** 근거 원문 (추출 본문·트랜스크립트 등). 빌더가 30,000자 컷. */
body: string;
/** [소스 메타] 블록에 표시할 키-값 (URL·참여자·기간 등). */
meta?: Record<string, string>;
/** frontmatter tags (kind 는 자동 포함). */
tags?: string[];
/** raw_sources / [S1] 표기 값 (예: URL, "email:제목"). */
sourceRef: string;
/** 소스 종류별 특수 규칙 (번호는 빌더가 5번부터 이어붙임). */
extraRules?: string[];
/** source_trust_level 평가 힌트 한 줄. */
trustHint?: string;
/** 변경 이력 문구. */
origin?: string;
}
/** 정본 골격 placeholder 치환 (wikifyPrompt.fill 과 동일 규칙). */
function fill(tpl: string, vars: Record<string, string>): string {
return tpl.replace(/\{\{(\w+)\}\}/g, (_, k) => vars[k] ?? '');
}
/**
* P-Reinforce 정본 지식 문서 프롬프트 — 모든 지식화 기능의 단일 진입점.
* canonical 미제공(null) 시 wikifyPrompt 내장 사본 사용.
*/
export function buildKnowledgeWikiPrompt(src: KnowledgeSource, canonical?: CanonicalWikiFormat | null): string {
// 내장 사본은 wikifyPrompt 가 소유 — 순환 없이 lazy require.
const { EMBEDDED_FALLBACK } = require('./prompts/wikifyPrompt') as { EMBEDDED_FALLBACK: CanonicalWikiFormat };
const fmt = canonical && canonical.commonRules && canonical.frontmatter && canonical.sections
? canonical : EMBEDDED_FALLBACK;
const today = new Date().toISOString().slice(0, 10);
const topic = (src.topic || `${src.kind} 지식`).trim();
const tags = [...new Set([src.kind, ...(src.tags || [])])].map(t => `"${t}"`).join(', ');
const vars = {
ID: slugifyId(topic, `${src.kind}-wiki`), TITLE: topic, TOPIC: topic, ROOT: topic, TODAY: today,
TAGS: tags, SOURCES: `[${JSON.stringify(src.sourceRef)}]`,
ORIGIN: src.origin || `Astra ${src.kind} 지식화로 초안 생성.`,
};
const baseRules = [
`1. 반드시 아래 [소스 본문]의 내용만을 근거로 작성하시오. 외부 지식을 절대 섞지 마시오.`,
`2. 반드시 아래 Markdown 템플릿과 Frontmatter 형식을 정확히 따르시오. 섹션 이름과 구조를 변경하지 마시오.`,
`3. 본문에 없는 정보는 지어내지 말고, 해당 섹션에 "소스에서 확인되지 않음"이라고 명시하시오. 수치·날짜·금액·고유명사는 원문 그대로 보존하시오.`,
`4. 한국어로 작성하시오. 관련 개념·고유명사는 [[대괄호 두 개]]로 감싸 위키 링크로 만드시오. 위키 링크는 반드시 \`[[\` 로 열고 \`]]\` 로 닫으시오.`,
];
const extra = (src.extraRules || []).map((r, i) => `${5 + i}. ${r}`);
const trustRule = `${5 + extra.length}. 이 문서의 출처는 ${src.kind} 소스 1건이므로 '## 📚 출처'의 [S1]은 ${src.sourceRef} 를 가리키며, source_trust_level 은 ${src.trustHint || '소스 성격에 따라 정직하게'} 평가하시오.`;
const metaLines = Object.entries(src.meta || {}).map(([k, v]) => `- ${k}: ${v || '(없음)'}`);
return `임무: 아래 ${src.kind} 소스를 근거로 P-Reinforce v${fmt.version.replace('-embedded', '')} 규격에 맞춰 고밀도 지식 문서를 작성하시오.
주제: '${topic}'
[필수 규칙]
${[...baseRules, ...extra, trustRule].join('\n')}
${fill(fmt.commonRules, vars)}
[소스 메타]
${metaLines.join('\n') || '- (메타 없음)'}
[소스 본문]
\`\`\`
${String(src.body || '').slice(0, 30000)}
\`\`\`
[출력 템플릿 - 이 형식을 정확히 따르시오]
${fill(fmt.frontmatter, vars)}
${fill(fmt.sections, vars)}`;
}