feat(meet): 회의록 가이드 v2 반영 + 모델 출력 붕괴 복원력 (v2.2.253)
회의록 출력물 개선 (실무 회의록 가이드 v2): - 섹션 우선순위 재정렬(①결정 ②액션 ③오픈이슈 ④리스크 ⑤논의) - 논의사항 주제별 bullet 간결화, 오픈 이슈 섹션 복원 - 액션 아이템 산출물 컬럼 추가(담당·작업·기한·산출물 4요소), 담당자 개인 우선 - Executive Summary 결과 중심, 결정사항은 확정된 것만 모델 출력 붕괴(degeneration) 대응: - callLmSynthesis 재시도 내장(repeat_penalty↑/top_k↓로 반복 억제 강화) + looksDegenerate 감지 - 긴 녹취 조각 실패 시 절반 분할 재귀 재시도(12K→6K→3.5K) - 부분 회의록 fallback(한 조각 실패해도 전체 중단 안 함) 하위호환: 액션 표 파서 신6컬럼/구5컬럼 모두 파싱, 섹션 번호 무관 탐지, 회의일 추출 일시/날짜 둘 다 인식. 테스트 +13건(전체 659 통과). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -31,9 +31,9 @@ export function toYmd(d: Date): string {
|
||||
return `${y}-${m}-${day}`;
|
||||
}
|
||||
|
||||
/** 회의록 본문의 "**날짜**: 2026년 05월 08일"에서 회의 날짜 추출. 없으면 fallback. */
|
||||
/** 회의록 본문의 "**일시**: 2026년 05월 08일"(구 형식 "**날짜**:" 도 호환)에서 회의 날짜 추출. 없으면 fallback. */
|
||||
export function extractMeetingDate(report: string, fallback: Date): Date {
|
||||
const m = report.match(/날짜\*{0,2}\s*[::]\s*\*{0,2}\s*(\d{4})\s*년\s*(\d{1,2})\s*월\s*(\d{1,2})\s*일/);
|
||||
const m = report.match(/(?:일시|날짜)\*{0,2}\s*[::]\s*\*{0,2}\s*(\d{4})\s*년\s*(\d{1,2})\s*월\s*(\d{1,2})\s*일/);
|
||||
if (m) {
|
||||
const d = new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
|
||||
if (!isNaN(d.getTime())) return d;
|
||||
@@ -75,11 +75,11 @@ export function resolveTaskDate(due: string, meetingDate: Date, today: Date): {
|
||||
* 5열 신표(담당 | 작업 내용 | 작업 상세 | 기한 | 상태) · 4열(상태 없음) ·
|
||||
* 구(舊) 3열 표(담당 | 작업 내용 | 기한)를 모두 지원한다. 누락 컬럼은 빈 문자열.
|
||||
*/
|
||||
export function parseActionItems(report: string): { owner: string; work: string; detail: string; due: string; status: string }[] {
|
||||
const rows: { owner: string; work: string; detail: string; due: string; status: string }[] = [];
|
||||
export function parseActionItems(report: string): { owner: string; work: string; detail: string; deliverable: string; due: string; status: string }[] {
|
||||
const rows: { owner: string; work: string; detail: string; deliverable: string; due: string; status: string }[] = [];
|
||||
let inSection = false;
|
||||
for (const line of report.split('\n')) {
|
||||
if (/^#{1,6}\s*5\.\s*액션\s*아이템/.test(line)) { inSection = true; continue; }
|
||||
if (/^#{1,6}\s*(?:\d+\.\s*)?액션\s*아이템/.test(line)) { inSection = true; continue; }
|
||||
if (!inSection) continue;
|
||||
if (/^#{1,6}\s/.test(line)) break; // 다음 섹션 시작 → 종료
|
||||
if (!/^\s*\|/.test(line)) continue;
|
||||
@@ -87,12 +87,16 @@ export function parseActionItems(report: string): { owner: string; work: string;
|
||||
if (cells.length < 3) continue;
|
||||
if (/^:?-+:?$/.test(cells[0])) continue; // 표 구분선
|
||||
if (cells[0] === '담당' || cells[1] === '작업 내용') continue; // 헤더
|
||||
if (cells.length >= 5) {
|
||||
rows.push({ owner: cells[0], work: cells[1], detail: cells[2], due: cells[3], status: cells[4] });
|
||||
if (cells.length >= 6) {
|
||||
// 신 형식: 담당 | 작업 내용 | 작업 상세 | 산출물 | 기한 | 상태
|
||||
rows.push({ owner: cells[0], work: cells[1], detail: cells[2], deliverable: cells[3], due: cells[4], status: cells[5] });
|
||||
} else if (cells.length === 5) {
|
||||
// 구 형식: 담당 | 작업 내용 | 작업 상세 | 기한 | 상태 (산출물 컬럼 없음)
|
||||
rows.push({ owner: cells[0], work: cells[1], detail: cells[2], deliverable: '', due: cells[3], status: cells[4] });
|
||||
} else if (cells.length === 4) {
|
||||
rows.push({ owner: cells[0], work: cells[1], detail: cells[2], due: cells[3], status: '' });
|
||||
rows.push({ owner: cells[0], work: cells[1], detail: cells[2], deliverable: '', due: cells[3], status: '' });
|
||||
} else {
|
||||
rows.push({ owner: cells[0], work: cells[1], detail: '', due: cells[2], status: '' });
|
||||
rows.push({ owner: cells[0], work: cells[1], detail: '', deliverable: '', due: cells[2], status: '' });
|
||||
}
|
||||
}
|
||||
return rows;
|
||||
|
||||
@@ -29,7 +29,7 @@ import { resolveTaskDate, toYmd, addBusinessDays } from './calendarHelpers';
|
||||
import { logInfo } from '../../../utils';
|
||||
|
||||
// ── 타입 ────────────────────────────────────────────────────────────────────
|
||||
export type ActionRow = { owner: string; work: string; detail: string; due: string; status: string };
|
||||
export type ActionRow = { owner: string; work: string; detail: string; deliverable: string; due: string; status: string };
|
||||
|
||||
export type HoldKind = 'undecided' | 'nodate' | 'conditional';
|
||||
export interface PendingItem {
|
||||
@@ -37,6 +37,7 @@ export interface PendingItem {
|
||||
owner: string;
|
||||
work: string;
|
||||
detail: string;
|
||||
deliverable: string; // 산출물 (없으면 "확인 필요")
|
||||
due: string;
|
||||
kind: HoldKind;
|
||||
condition?: string; // kind=conditional 의 선행작업
|
||||
@@ -185,10 +186,12 @@ export async function registerAction(
|
||||
}
|
||||
|
||||
/** 등록 노트 공통 빌더. */
|
||||
export function buildNotes(p: { detail: string; meetTitle: string; owner: string; dueRaw: string; dateLabel: string; extra?: string[] }): string {
|
||||
export function buildNotes(p: { detail: string; meetTitle: string; owner: string; deliverable?: string; dueRaw: string; dateLabel: string; extra?: string[] }): string {
|
||||
const detailLine = p.detail?.trim() || '(녹취록에서 작업 상세가 추출되지 않음 — 회의록 본문 참조)';
|
||||
const deliverable = p.deliverable?.trim();
|
||||
return [
|
||||
'■ 작업 상세', detailLine, '',
|
||||
...(deliverable ? ['■ 산출물', deliverable, ''] : []),
|
||||
...(p.extra && p.extra.length ? [...p.extra, ''] : []),
|
||||
'■ 맥락',
|
||||
`· 회의록: ${p.meetTitle}`,
|
||||
@@ -325,7 +328,7 @@ export async function processConfirmDecisions(
|
||||
}
|
||||
|
||||
const notes = buildNotes({
|
||||
detail: item.detail, meetTitle: pend.meetTitle, owner: item.owner,
|
||||
detail: item.detail, meetTitle: pend.meetTitle, owner: item.owner, deliverable: item.deliverable,
|
||||
dueRaw: item.due, dateLabel: date || '(날짜 없음 — 조건부)', extra,
|
||||
});
|
||||
const r = await registerAction(context, {
|
||||
|
||||
Reference in New Issue
Block a user