v2.2.299~307: 아키텍처 수렴 + 채팅 정리 + 벤치마킹 강화 + Claude 구독 엔진
- v2.2.299 아키텍처 수렴: coreChat 통일(엔진 휴리스틱 3벌 제거), 기업 모드 검색 오케스트레이터 승격, lib/execUtil(실행 래퍼 6곳·Python 탐지 3벌 단일화), lib/kstSchedule(워처 4개 nowInKst 통합), estimateTokens 통합, 설정 접근 규칙 명문화 - v2.2.300 채팅 화면 정리: LiveReasoningFilter(스트리밍 중 <think>/Harmony 추론 토큰 단위 차단), 확신도·검토요청 footer 기본 숨김(계산·Reflection 은 유지) - v2.2.301 문맥·의도 이해: [답변 전 이해 원칙] 상시 주입, 워크플로우 의도 브리핑, Report QA 루프(규칙 레지스트리+실측치+회귀 게이트, 블로그_v3 개념 이식) - v2.2.302 /benchmark 비즈니스 렌즈(가격·수익·운영)+빌드 프롬프트 모드+QA 연계 - v2.2.303 handoff 모드(측정치 무손실 인수인계 문서)+/claude(Claude Code 터미널 위임) - v2.2.304 이식성: 지식 경로 두뇌-상대 규약(pickWikiDir 상대 해석), 이사 체크리스트 - v2.2.305 Claude 구독 엔진: claude: 프로바이더(CLI 위임, 모델 드롭다운 자동 노출, coreChat 지원 — 워크플로우·QA도 구독 모델 가능) - v2.2.306 Tone Guard: AI 상투어 금지 레지스트리(상담사 화법 실사례 8종+대조 예시) - v2.2.307 /benchmark 레이아웃 골격(sectionRoles 결정론 분석, 롤링 배너 즉답), 파트별 실패 격리, 합성 타임아웃 120→300초 검증: tsc 무오류 + jest 888 통과 + esbuild 정상 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -15,10 +15,15 @@ import { promises as fsp } from 'fs';
|
||||
import * as path from 'path';
|
||||
import { registerSlashCommand, chunk, type Webview } from './slashRouter';
|
||||
import { callLmSynthesis } from './llm';
|
||||
import { getConfig } from '../../config';
|
||||
import { logInfo } from '../../utils';
|
||||
import { runReportQaLoop } from '../../agents/reportQa';
|
||||
import { bridgeFetch, BRIDGE_API } from './bridgeClient';
|
||||
import { collectSourceFiles } from './reviewFiles';
|
||||
import { buildReviewFilePrompt, buildReviewReducePrompt } from './prompts/reviewPrompt';
|
||||
import { type SynthesisPart, buildSynthesisPrompt } from './prompts/synthesisPrompt';
|
||||
import { buildHandoffDoc } from './prompts/handoffDoc';
|
||||
import { buildLayoutSkeleton } from './prompts/layoutSkeleton';
|
||||
import {
|
||||
type YoutubeAnalysisMode,
|
||||
formatHms,
|
||||
@@ -49,28 +54,64 @@ import {
|
||||
|
||||
// ───────────────────────────── /benchmark ─────────────────────────────
|
||||
|
||||
async function runBenchmark(arg: string, view: Webview | undefined): Promise<boolean> {
|
||||
/**
|
||||
* /benchmark 모드 → 실행할 합성 파트 목록.
|
||||
* - full(기본): 디자인 3파트 + 비즈니스 렌즈 (1~4)
|
||||
* - design: 기존 3파트만 (원본 복원 시방서)
|
||||
* - biz: 비즈니스 렌즈만 (빠름 — 포지셔닝·가격·운영)
|
||||
* - build: 풀코스 + 스텝별 빌드 프롬프트 (1~5)
|
||||
*/
|
||||
export type BenchmarkMode = 'full' | 'design' | 'biz' | 'build' | 'handoff';
|
||||
export const BENCHMARK_MODE_PARTS: Record<BenchmarkMode, ReadonlyArray<1 | 2 | 3 | 4 | 5>> = {
|
||||
full: [1, 2, 3, 4],
|
||||
design: [1, 2, 3],
|
||||
biz: [4],
|
||||
build: [1, 2, 3, 4, 5],
|
||||
handoff: [], // LLM 합성 없음 — 스캔 원시 데이터로 인수인계 문서를 결정론 생성
|
||||
};
|
||||
|
||||
/** /benchmark 인자 파싱 — 순수 함수 (테스트 대상). */
|
||||
export function parseBenchmarkArgs(arg: string): {
|
||||
url: string; depth?: number; pages?: number; mode: BenchmarkMode; userContent: string;
|
||||
} {
|
||||
const tokens = arg.trim().split(/\s+/).filter(Boolean);
|
||||
let url = '';
|
||||
let depthArg: number | undefined;
|
||||
let pagesArg: number | undefined;
|
||||
let depth: number | undefined;
|
||||
let pages: number | undefined;
|
||||
let mode: BenchmarkMode = 'full';
|
||||
const restParts: string[] = [];
|
||||
const BARE_MODES = new Set<BenchmarkMode>(['full', 'design', 'biz', 'build', 'handoff']);
|
||||
for (const t of tokens) {
|
||||
const m = /^(depth|pages)=(\d+)$/i.exec(t);
|
||||
const modeM = /^mode[:=](full|design|biz|build|handoff)$/i.exec(t);
|
||||
if (m) {
|
||||
if (m[1].toLowerCase() === 'depth') depthArg = Number(m[2]);
|
||||
else pagesArg = Number(m[2]);
|
||||
if (m[1].toLowerCase() === 'depth') depth = Number(m[2]);
|
||||
else pages = Number(m[2]);
|
||||
} else if (modeM) {
|
||||
mode = modeM[1].toLowerCase() as BenchmarkMode;
|
||||
} else if (BARE_MODES.has(t.toLowerCase() as BenchmarkMode)) {
|
||||
mode = t.toLowerCase() as BenchmarkMode;
|
||||
} else if (!url) {
|
||||
url = t;
|
||||
} else {
|
||||
restParts.push(t);
|
||||
}
|
||||
}
|
||||
return { url, depth, pages, mode, userContent: restParts.join(' ') };
|
||||
}
|
||||
|
||||
async function runBenchmark(arg: string, view: Webview | undefined): Promise<boolean> {
|
||||
const { url, depth: depthArg, pages: pagesArg, mode, userContent } = parseBenchmarkArgs(arg);
|
||||
if (!url) {
|
||||
chunk(view, `사용법: \`/benchmark <url> [depth=N] [pages=N] [보조 설명]\`\n예: \`/benchmark https://example.com depth=2 pages=12\`\n`);
|
||||
chunk(view, [
|
||||
'사용법: `/benchmark <url> [mode] [depth=N] [pages=N] [보조 설명]`',
|
||||
'- mode: `full`(기본 — 디자인+비즈니스) · `design`(복원 시방서만) · `biz`(사업 분석만) · `build`(풀코스+빌드 프롬프트) · `handoff`(클라우드 개발용 인수인계 문서 — LLM 없이 즉시)',
|
||||
'- 가격·수익 분석이 목적이면 `depth=2` 로 pricing 페이지까지 크롤 권장',
|
||||
'예: `/benchmark https://interiorai.com build depth=2 pages=12 나는 부동산 쪽으로 변형하고 싶어`',
|
||||
'',
|
||||
].join('\n'));
|
||||
return true;
|
||||
}
|
||||
const userContent = restParts.join(' ');
|
||||
|
||||
const cfg = vscode.workspace.getConfiguration('g1nation');
|
||||
const crawlDepth = depthArg ?? (cfg.get<number>('datacollectCrawlDepth', 1) ?? 1);
|
||||
@@ -126,23 +167,84 @@ async function runBenchmark(arg: string, view: Webview | undefined): Promise<boo
|
||||
'```',
|
||||
].join('\n');
|
||||
|
||||
// ── handoff 모드: LLM 합성을 건너뛰고 스캔 원시 데이터로 인수인계 문서를 즉시 생성.
|
||||
// 소형 모델의 요약 병목을 통과시키지 않는다 (측정치 무손실 — handoffDoc.ts 참조).
|
||||
if (mode === 'handoff') {
|
||||
const handoffDoc = buildHandoffDoc(s, url, { crawlDepth, maxPages, userContent });
|
||||
chunk(view, `📦 **개발 인수인계 문서 생성** (LLM 미개입 — 측정치 원본)\n\n`);
|
||||
const hToday = new Date().toISOString().slice(0, 10);
|
||||
let hHost = url;
|
||||
try { hHost = new URL(/^https?:\/\//i.test(url) ? url : `https://${url}`).host; } catch { /* keep raw */ }
|
||||
const hTitle = `웹벤치마크 핸드오프 ${hHost} ${hToday}`;
|
||||
const savedH = await persistWikiDoc(hTitle, handoffDoc, { docType: 'benchmark-handoff', sources: [url] });
|
||||
chunk(view, describeSave(savedH, '인수인계 문서'));
|
||||
if (savedH.ok && savedH.path) {
|
||||
chunk(view, [
|
||||
'',
|
||||
'**다음 단계 — 클라우드 모델로 개발:**',
|
||||
`1. 파일을 열어 §1 "내 요구사항" 을 채우세요 (무엇으로 변형할지).`,
|
||||
`2. Claude Code(구독 인증)로 바로 개발: \`/claude ${savedH.path} 파일을 읽고 명세대로 개발해줘\``,
|
||||
` (또는 Antigravity 에 파일 내용을 붙여넣어도 됩니다)`,
|
||||
'',
|
||||
].join('\n'));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
let finalReport: string;
|
||||
if (looksEmpty) {
|
||||
chunk(view, `(스캔이 비어 LLM 합성을 건너뜁니다.)\n\n`);
|
||||
finalReport = rawReport;
|
||||
} else {
|
||||
const model = (cfg.get<string>('defaultModel', '') || 'gemma4:e2b').trim();
|
||||
chunk(view, `🧪 **LLM 4-렌즈 합성** (3단계 · 모델 \`${model}\`)\n모델·하드웨어에 따라 수 분 걸릴 수 있습니다…\n`);
|
||||
const partList = BENCHMARK_MODE_PARTS[mode];
|
||||
const PART_LABELS: Record<number, string> = {
|
||||
1: '4-렌즈 디자인', 2: 'IA·토큰', 3: '재구축 명세', 4: '비즈니스 렌즈', 5: '빌드 프롬프트',
|
||||
};
|
||||
chunk(view, `🧪 **LLM 합성** (mode \`${mode}\` · ${partList.length}단계 · 모델 \`${model}\`)\n모델·하드웨어에 따라 수 분 걸릴 수 있습니다…\n`);
|
||||
try {
|
||||
const parts: string[] = [];
|
||||
for (const part of [1, 2, 3] as const) {
|
||||
chunk(view, `\n · 합성 ${part}/3 진행 중…`);
|
||||
let okParts = 0;
|
||||
for (let i = 0; i < partList.length; i++) {
|
||||
const part = partList[i];
|
||||
chunk(view, `\n · ${i + 1}/${partList.length} ${PART_LABELS[part]} 합성 중…`);
|
||||
const partT0 = Date.now();
|
||||
const out = await callLmSynthesis(buildSynthesisPrompt(s, userContent, part));
|
||||
if (!out) throw new Error(`LLM ${part}/3 응답이 비어 있습니다.`);
|
||||
// [v2.2.307] 파트별 실패 격리 — 한 파트가 죽어도 완성된 파트를 버리지 않는다
|
||||
// (실사례: 파트 2 타임아웃으로 105초 걸린 파트 1까지 폐기). 타임아웃도 300초로 —
|
||||
// 입력 JSON 이 큰 파트는 e4b 급에서 120초를 넘긴다.
|
||||
let out: string;
|
||||
try {
|
||||
out = await callLmSynthesis(buildSynthesisPrompt(s, userContent, part), undefined, { timeoutMs: 300_000 });
|
||||
} catch (partErr: any) {
|
||||
chunk(view, ` ✗ (${Math.round((Date.now() - partT0) / 1000)}s — ${partErr?.message || partErr})`);
|
||||
parts.push(`> ⚠️ **${PART_LABELS[part]} 파트 합성 실패**: ${partErr?.message || String(partErr)} — 나머지 파트는 계속 진행됨.`);
|
||||
continue;
|
||||
}
|
||||
if (!out) {
|
||||
chunk(view, ` ✗ (빈 응답)`);
|
||||
parts.push(`> ⚠️ **${PART_LABELS[part]} 파트 합성 실패**: 빈 응답 — 나머지 파트는 계속 진행됨.`);
|
||||
continue;
|
||||
}
|
||||
okParts++;
|
||||
// [Report QA] 비즈니스 렌즈는 날조 위험(임의 가격·매출)이 가장 큰 파트 —
|
||||
// 채점→피드백 재작성→회귀 게이트를 통과시킨다. prompt 로 합성 프롬프트
|
||||
// 자체(스캔 JSON 포함)를 넘겨 재작성이 원본 근거를 보게 한다.
|
||||
if (part === 4 && getConfig().reportQaEnabled) {
|
||||
chunk(view, ` (품질 검증 중…)`);
|
||||
const qa = await runReportQaLoop({
|
||||
draft: out,
|
||||
prompt: buildSynthesisPrompt(s, userContent, 4),
|
||||
intentBrief: null,
|
||||
model,
|
||||
onProgress: (msg: string) => chunk(view, ` · ${msg}`),
|
||||
});
|
||||
out = qa.finalDraft;
|
||||
logInfo('[Benchmark] 비즈니스 렌즈 QA 완료.', { score: qa.score, revisions: qa.revisions });
|
||||
}
|
||||
parts.push(out);
|
||||
chunk(view, ` ✓ (${Math.round((Date.now() - partT0) / 1000)}s)`);
|
||||
}
|
||||
if (okParts === 0) throw new Error('모든 합성 파트가 실패했습니다.');
|
||||
finalReport = parts.join('\n\n---\n\n');
|
||||
chunk(view, `\n\n`);
|
||||
} catch (e: any) {
|
||||
@@ -151,6 +253,10 @@ async function runBenchmark(arg: string, view: Webview | undefined): Promise<boo
|
||||
}
|
||||
}
|
||||
|
||||
// [v2.2.307] 레이아웃 골격 (결정론) — 합성 성공/실패와 무관하게 항상 포함.
|
||||
// "배너/캐러셀이 있는가, 각 페이지가 어떤 블록 순서로 구성되는가"에 즉답.
|
||||
finalReport = `${buildLayoutSkeleton(s)}\n\n---\n\n${finalReport}`;
|
||||
|
||||
chunk(view, finalReport + '\n\n');
|
||||
|
||||
try {
|
||||
@@ -1129,7 +1235,7 @@ async function reduceReviewNotes(
|
||||
// /research(NotebookLM Deep Research)는 v2.2.205 에서 제거 — NotebookLM 은 로컬
|
||||
// Datacollect 앱 전용으로 분리(Chrome/Google 로그인 의존). ASTRA 백엔드는 NAS 경량
|
||||
// Bridge 로 운영 가능해야 하므로 brower-auth 가 필요한 명령은 두지 않는다.
|
||||
registerSlashCommand({ name: '/benchmark', description: 'Playwright 웹 벤치마크 + 4-렌즈 LLM 분석', handler: runBenchmark });
|
||||
registerSlashCommand({ name: '/benchmark', description: '웹 벤치마크 — 디자인 4-렌즈 + 비즈니스 렌즈(가격·수익·운영) 분석, build 모드는 스텝별 빌드 프롬프트까지', handler: runBenchmark });
|
||||
registerSlashCommand({ name: '/youtube', description: 'YouTube 단일 영상 또는 채널/플레이리스트 분석', handler: runYoutube });
|
||||
registerSlashCommand({ name: '/blog', description: 'Blog Pipeline 안내 (Datacollect 별도 흐름)', handler: runBlog });
|
||||
registerSlashCommand({ name: '/wikify', description: '웹 URL → P-Reinforce v3.0 위키 합성·저장', handler: runWikify });
|
||||
|
||||
Reference in New Issue
Block a user