fa397d7fa1
[/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>
177 lines
9.0 KiB
TypeScript
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)}`;
|
|
}
|