diff --git a/.astra/tests/stress/.astra/cache/259a37934ead3910a8722b82054d46d2ca2057b05c488be1dcf439166ac5a9a1.json b/.astra/tests/stress/.astra/cache/259a37934ead3910a8722b82054d46d2ca2057b05c488be1dcf439166ac5a9a1.json index c1f8a30..b295698 100644 --- a/.astra/tests/stress/.astra/cache/259a37934ead3910a8722b82054d46d2ca2057b05c488be1dcf439166ac5a9a1.json +++ b/.astra/tests/stress/.astra/cache/259a37934ead3910a8722b82054d46d2ca2057b05c488be1dcf439166ac5a9a1.json @@ -1,5 +1,5 @@ { "result": "Final report with inconsistencies. This should be long enough to pass validation.", - "createdAt": 1779084064068, + "createdAt": 1779179392149, "modelVersion": "unknown" } \ No newline at end of file diff --git a/.astra/tests/stress/.astra/cache/65775be352df43297b63c7af59c9f4f39d2bc368f77456c37b5eef9a94a66b5c.json b/.astra/tests/stress/.astra/cache/65775be352df43297b63c7af59c9f4f39d2bc368f77456c37b5eef9a94a66b5c.json index 6f6a02c..e5983c9 100644 --- a/.astra/tests/stress/.astra/cache/65775be352df43297b63c7af59c9f4f39d2bc368f77456c37b5eef9a94a66b5c.json +++ b/.astra/tests/stress/.astra/cache/65775be352df43297b63c7af59c9f4f39d2bc368f77456c37b5eef9a94a66b5c.json @@ -1,5 +1,5 @@ { "result": "[CONFLICT WARNING] 성능이 200% 증가했습니다. vs 그러나 동시에 50% 감소했습니다. 최적화와 성능 저하가 동시에 발견됨.", - "createdAt": 1779084064066, + "createdAt": 1779179392147, "modelVersion": "unknown" } \ No newline at end of file diff --git a/.astra/tests/stress/.astra/cache/6894d26c5b0a55d25d756a473225c7a44d7661af673b24e3f49551a7a2e50280.json b/.astra/tests/stress/.astra/cache/6894d26c5b0a55d25d756a473225c7a44d7661af673b24e3f49551a7a2e50280.json index f08ca6a..46c62d3 100644 --- a/.astra/tests/stress/.astra/cache/6894d26c5b0a55d25d756a473225c7a44d7661af673b24e3f49551a7a2e50280.json +++ b/.astra/tests/stress/.astra/cache/6894d26c5b0a55d25d756a473225c7a44d7661af673b24e3f49551a7a2e50280.json @@ -1,5 +1,5 @@ { "result": "Detailed Execution Plan: 1. Research 2. Analyze 3. Write report with high quality.", - "createdAt": 1779084064063, + "createdAt": 1779179392144, "modelVersion": "unknown" } \ No newline at end of file diff --git a/.astra/tests/stress/.astra/cache/88cb61499f88ed38165b64bd3e8adc543795e4b427b64540a49c9ab27c7fe213.json b/.astra/tests/stress/.astra/cache/88cb61499f88ed38165b64bd3e8adc543795e4b427b64540a49c9ab27c7fe213.json index 9a622dc..523f0e3 100644 --- a/.astra/tests/stress/.astra/cache/88cb61499f88ed38165b64bd3e8adc543795e4b427b64540a49c9ab27c7fe213.json +++ b/.astra/tests/stress/.astra/cache/88cb61499f88ed38165b64bd3e8adc543795e4b427b64540a49c9ab27c7fe213.json @@ -1,5 +1,5 @@ { - "result": "---\nid: stress_conflict_1779084064049\ndate: 2026-05-18T06:01:04.070Z\ntype: knowledge_artifact\nstandard: P-Reinforce v3.0\ntags: [automated, connect_ai, brain_sync]\n---\n\n## 📌 Brief Summary\nFinal report with inconsistencies. This should be long enough to pass validation.\n\nFinal report with inconsistencies. This should be long enough to pass validation.\n\n---\n## 💡 Astra의 선제적 제안 (Proactive Next Actions)\nFinal report with inconsistencies. This should be long enough to pass validation.\n---\n## 🛡️ Reliability & Audit Summary\n> [!NOTE]\n> 이 문서는 ConnectAI의 **Intelligent Resilience** 엔진에 의해 검증 및 정제되었습니다.\n\n| Metric | Value | Status |\n| :--- | :--- | :--- |\n| **Conflict Risk** | `60/100` | ⚠️ Medium |\n| **Fallbacks Used** | `0` | ✅ None |\n| **Auto Retries** | `0` | ✅ Stable |\n| **Deduplication** | `0` | Standard |\n| **Processing Time** | `0.0s` | ✅ Fast |\n\n### 🔍 Decision Audit Trail\n- **[PLANNER]** 전략 수립 중... (13ms)\n- **[RESEARCHER]** 핵심 정보 수집 및 분석 중... (2ms)\n- **[WRITER]** 최종 리포트 작성 및 편집 중... (3ms)\n", - "createdAt": 1779084064070, + "result": "---\nid: stress_conflict_1779179392129\ndate: 2026-05-19T08:29:52.151Z\ntype: knowledge_artifact\nstandard: P-Reinforce v3.0\ntags: [automated, connect_ai, brain_sync]\n---\n\n## 📌 Brief Summary\nFinal report with inconsistencies. This should be long enough to pass validation.\n\nFinal report with inconsistencies. This should be long enough to pass validation.\n\n---\n## 💡 Astra의 선제적 제안 (Proactive Next Actions)\nFinal report with inconsistencies. This should be long enough to pass validation.\n---\n## 🛡️ Reliability & Audit Summary\n> [!NOTE]\n> 이 문서는 ConnectAI의 **Intelligent Resilience** 엔진에 의해 검증 및 정제되었습니다.\n\n| Metric | Value | Status |\n| :--- | :--- | :--- |\n| **Conflict Risk** | `60/100` | ⚠️ Medium |\n| **Fallbacks Used** | `0` | ✅ None |\n| **Auto Retries** | `0` | ✅ Stable |\n| **Deduplication** | `0` | Standard |\n| **Processing Time** | `0.0s` | ✅ Fast |\n\n### 🔍 Decision Audit Trail\n- **[PLANNER]** 전략 수립 중... (14ms)\n- **[RESEARCHER]** 핵심 정보 수집 및 분석 중... (2ms)\n- **[WRITER]** 최종 리포트 작성 및 편집 중... (3ms)\n", + "createdAt": 1779179392151, "modelVersion": "unknown" } \ No newline at end of file diff --git a/.astra/tests/stress/.astra/missions/stress_conflict_1779084064049.json b/.astra/tests/stress/.astra/missions/stress_conflict_1779179392129.json similarity index 82% rename from .astra/tests/stress/.astra/missions/stress_conflict_1779084064049.json rename to .astra/tests/stress/.astra/missions/stress_conflict_1779179392129.json index a9400d3..075c1e8 100644 --- a/.astra/tests/stress/.astra/missions/stress_conflict_1779084064049.json +++ b/.astra/tests/stress/.astra/missions/stress_conflict_1779179392129.json @@ -1,8 +1,8 @@ { - "missionId": "stress_conflict_1779084064049", + "missionId": "stress_conflict_1779179392129", "status": "completed", - "startTime": "2026-05-18T06:01:04.049Z", - "totalElapsedMs": 22, + "startTime": "2026-05-19T08:29:52.129Z", + "totalElapsedMs": 23, "results": { "planner": "Detailed Execution Plan: 1. Research 2. Analyze 3. Write report with high quality.", "researcher": "[CONFLICT WARNING] 성능이 200% 증가했습니다. vs 그러나 동시에 50% 감소했습니다. 최적화와 성능 저하가 동시에 발견됨.", @@ -16,30 +16,30 @@ { "from": "idle", "to": "planner", - "durationMs": 13, + "durationMs": 14, "message": "전략 수립 중...", - "ts": "2026-05-18T06:01:04.062Z" + "ts": "2026-05-19T08:29:52.143Z" }, { "from": "planner", "to": "researcher", "durationMs": 2, "message": "핵심 정보 수집 및 분석 중...", - "ts": "2026-05-18T06:01:04.064Z" + "ts": "2026-05-19T08:29:52.145Z" }, { "from": "researcher", "to": "writer", "durationMs": 3, "message": "최종 리포트 작성 및 편집 중...", - "ts": "2026-05-18T06:01:04.067Z" + "ts": "2026-05-19T08:29:52.148Z" }, { "from": "writer", "to": "completed", "durationMs": 4, "message": "미션 완료", - "ts": "2026-05-18T06:01:04.071Z" + "ts": "2026-05-19T08:29:52.152Z" } ], "resilienceMetrics": { diff --git a/PATCHNOTES.md b/PATCHNOTES.md index c563a8e..2a1d731 100644 --- a/PATCHNOTES.md +++ b/PATCHNOTES.md @@ -1,5 +1,41 @@ # Astra Patch Notes +## v2.2.30 (2026-05-19) +### 🔓 Datacollect Radio: 슬래시 명령 후 채팅 input 자동 해제 +- **슬래시 명령(`/research`, `/benchmark`, `/youtube`, `/blog`) 종료 시 `streamEnd` 신호 누락 수정.** Astra 채팅 input은 streamEnd 메시지로 잠금이 풀리는데, 우리 slashRouter는 일반 LLM streamer를 우회해 bridge를 직접 호출하므로 자동으로 신호가 안 가던 상태. 사용자가 `/benchmark`를 입력하면 결과가 채팅에 표시돼도 input이 영원히 잠긴 채 무한 로딩으로 보였습니다. +- **`try/finally`로 streamEnd 보장.** timeout / 에러 / 정상 종료 어떤 경로든 input이 풀리도록 강제. +- **신규 패키징:** `astra-2.2.30.vsix` 패키지로 배포합니다. + +--- + + + +## v2.2.29 (2026-05-19) +### ⏱️ Datacollect Radio: NotebookLM 리서치 타임아웃 완화 +- **`/research` 명령의 `import` 단계 타임아웃을 120s → 300s로 확대**: NotebookLM이 deep research 결과를 노트북 소스로 옮길 때 큰 리포트는 2~5분이 걸려 기존 120s cap에서 `TRANSIENT_TIMEOUT`으로 떨어지고 정작 백엔드는 정상 처리 중인 race가 보고돼 수정. +- **`synthesize` 단계 타임아웃 300s → 600s**: 큰 노트북의 LLM 합성이 5~10분 걸리는 경우 cover. +- **`status` polling 타임아웃 30s → 60s**: MCP 자식 프로세스가 stale일 때 30s 안에 응답 못 하는 사례 완화. (같은 이슈는 [Wiki/Datacollect 프로젝트](e:\Wiki\Datacollect)의 [engine.ts](e:\Wiki\Datacollect\src\lib\engine.ts)에도 동일한 값으로 수정 완료) +- **신규 패키징:** `astra-2.2.29.vsix` 패키지로 배포합니다. + +--- + + + +## v2.2.28 (2026-05-19) +### 📻 Datacollect Radio: 채팅 슬래시 명령으로 외부 도구 통합 +- **새 UI 버튼 없이 채팅 한 줄로 외부 파이프라인 호출:** Astra 채팅에서 `/research`, `/benchmark`, `/youtube`, `/blog` 슬래시 명령으로 별도의 [Wiki/Datacollect 프로젝트](e:\Wiki\Datacollect) bridge(기본 `http://127.0.0.1:3002`)의 무거운 기능을 직접 라우팅합니다. +- **/research \<주제\>:** NotebookLM Deep Research 전체 파이프라인(notebook 생성 → status polling → import → synthesize)을 채팅에서 한 번에 실행하고 결과 마크다운을 스트리밍으로 받습니다. +- **/benchmark \:** Playwright 기반 웹사이트 분석 — 디자인 토큰, 컬러 팔레트, 사이트맵 ASCII 다이어그램, 마이크로카피를 요약해 채팅에 표시. +- **/youtube \:** 영상 metadata + transcript를 추출해 챕터/태그/본문 미리보기까지 한 번에. +- **/blog \<키워드\>:** Datacollect Blog Pipeline 페이지(http://127.0.0.1:8787/blog/)를 자동 오픈 (Bridge에 대응 API가 추가되면 채팅 직접 실행도 지원 예정). +- **LLM 토큰 절약:** 슬래시 명령은 회사 모드/일반 chat 분기 *전에* 잡히므로 모델 비용 없이 처리됩니다. 진행상황·결과는 일반 LLM 응답과 같은 자리(`streamChunk`)에 표시. +- **설정 추가:** `g1nation.datacollectBridgeUrl` (default `http://127.0.0.1:3002`)로 bridge 위치 override 가능. Datacollect는 `npm run bridge`로 미리 띄워 두어야 합니다. +- **신규 패키징:** `astra-2.2.28.vsix` 패키지로 배포합니다. + +--- + + + ## v2.2.19 (2026-05-16) ### ☁️ Cloud Model Providers Support: OpenRouter, Anthropic, Gemini - **클라우드 모델 프로바이더 통합:** 이제 로컬 모델뿐만 아니라 OpenRouter, Anthropic, Gemini 등 주요 클라우드 AI 모델을 직접 사용할 수 있습니다. diff --git a/package.json b/package.json index 3b2e25b..5e66f57 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "astra", "displayName": "Astra", "description": "The personal intelligence layer for Antigravity and VS Code. A private cognitive partner for deep project context, memory, and proactive strategic decision-making.", - "version": "2.2.27", + "version": "2.2.30", "publisher": "g1nation", "license": "MIT", "icon": "assets/icon.png", @@ -199,6 +199,11 @@ "default": false, "description": "Enable Multi-Agent Workflow (Planner -> Researcher -> Writer) for complex tasks." }, + "g1nation.datacollectBridgeUrl": { + "type": "string", + "default": "http://127.0.0.1:3002", + "description": "Wiki/Datacollect MCP Bridge URL. /research, /benchmark, /youtube chat slash commands route here. The Bridge must be running (`npm run bridge` in the Datacollect project)." + }, "g1nation.memoryEnabled": { "type": "boolean", "default": true, diff --git a/src/features/datacollect/bridgeClient.ts b/src/features/datacollect/bridgeClient.ts new file mode 100644 index 0000000..1ff0298 --- /dev/null +++ b/src/features/datacollect/bridgeClient.ts @@ -0,0 +1,86 @@ +import * as vscode from 'vscode'; + +/** + * Datacollect (Wiki/Datacollect 프로젝트)의 MCP Bridge HTTP 클라이언트. + * + * Bridge는 사용자가 별도로 띄우는 Node Express 서버(`npm run bridge`)이고 + * 기본 포트는 3002. Research(NotebookLM)/Web Benchmark(Playwright)/YouTube + * (yt-dlp+transcript) 같은 무거운 기능을 노출하므로, Astra는 이 endpoint를 + * thin client로 호출만 한다 — Playwright/Chrome/NotebookLM-MCP 의존성을 + * Astra가 직접 들고 갈 필요 없음. + * + * URL은 `astra.datacollectBridgeUrl` VS Code 설정으로 override 가능, 기본값 + * `http://127.0.0.1:3002`. 사용자가 다른 머신/포트에서 띄우면 그쪽으로 가게. + */ + +export function getBridgeBaseUrl(): string { + const raw = vscode.workspace.getConfiguration('g1nation').get('datacollectBridgeUrl'); + const url = (raw && raw.trim()) || 'http://127.0.0.1:3002'; + return url.replace(/\/$/, ''); +} + +export interface BridgeFetchOptions { + timeoutMs?: number; + signal?: AbortSignal; +} + +/** + * Bridge endpoint를 호출하고 JSON 응답을 돌려준다. 5xx/4xx는 throw로 surface — + * `/api/research/start` 같은 핸들러가 `{ success: false, stage, error }` 형식의 + * 풍부한 진단 정보를 body에 담기 시작했으므로(이전 라운드 추가), throw 메시지에 + * `[stage] error` 형태로 포함해 slashRouter 쪽에서 사용자에게 그대로 보여준다. + */ +export async function bridgeFetch( + path: string, + init?: RequestInit, + opts: BridgeFetchOptions = {}, +): Promise { + const base = getBridgeBaseUrl(); + const url = `${base}${path.startsWith('/') ? path : `/${path}`}`; + + const controller = new AbortController(); + const timeoutMs = opts.timeoutMs ?? 60_000; + const timer = setTimeout(() => controller.abort(), timeoutMs); + + // 호출자 signal이 abort되면 그대로 forward. + if (opts.signal) { + if (opts.signal.aborted) controller.abort(); + else opts.signal.addEventListener('abort', () => controller.abort(), { once: true }); + } + + try { + const res = await fetch(url, { + ...init, + signal: controller.signal, + headers: { + 'Content-Type': 'application/json', + ...(init?.headers || {}), + }, + }); + const text = await res.text(); + let body: any = text; + try { body = JSON.parse(text); } catch { /* keep as text */ } + + if (!res.ok) { + const stage = body?.stage ? `[${body.stage}] ` : ''; + const errMsg = body?.error || body?.message || (typeof body === 'string' ? body : `HTTP ${res.status}`); + throw new Error(`Datacollect ${path} 실패: ${stage}${errMsg}`); + } + return body as T; + } catch (e: any) { + if (e?.name === 'AbortError') { + throw new Error(`Datacollect ${path} 시간 초과 (${timeoutMs}ms). Bridge가 떠 있는지 확인하세요 (${base}).`); + } + // ECONNREFUSED 등 connect 실패는 친절히 안내. + const msg = String(e?.message || e); + if (msg.includes('ECONNREFUSED') || msg.includes('fetch failed')) { + throw new Error( + `Datacollect Bridge(${base})에 연결할 수 없습니다. ` + + `Wiki/Datacollect 프로젝트에서 \`npm run bridge\`를 실행하고 다시 시도하세요.`, + ); + } + throw e; + } finally { + clearTimeout(timer); + } +} diff --git a/src/features/datacollect/slashRouter.ts b/src/features/datacollect/slashRouter.ts new file mode 100644 index 0000000..7ab59ac --- /dev/null +++ b/src/features/datacollect/slashRouter.ts @@ -0,0 +1,243 @@ +import * as vscode from 'vscode'; +import { bridgeFetch, getBridgeBaseUrl } from './bridgeClient'; + +/** + * Datacollect "라디오" slash 명령 라우터. + * + * 사용자가 Astra 채팅에서 `/research <주제>` 같은 입력을 보내면 chatHandlers에서 + * 이 모듈로 위임. 새 UI 버튼 없이 채팅 단일 경로만으로 Datacollect bridge의 + * 무거운 기능(NotebookLM Deep Research, Playwright 웹 벤치마크, YouTube 4-렌즈 + * 분석, Blog Pipeline)을 호출한다. + * + * 진행 상황과 최종 결과는 모두 webview에 `streamChunk` 메시지로 흘려, 일반 + * LLM 응답과 같은 자리에 자연스럽게 표시된다. + * + * 명령이 처리되면 true 반환 → chatHandlers가 일반 LLM 흐름으로 안 내려가게. + */ + +const COMMANDS = ['/research', '/benchmark', '/youtube', '/blog'] as const; +type SlashCommand = typeof COMMANDS[number]; + +export function isSlashCommand(input: string): boolean { + const trimmed = input.trim(); + if (!trimmed.startsWith('/')) return false; + const head = trimmed.split(/\s+/, 1)[0].toLowerCase(); + return (COMMANDS as readonly string[]).includes(head); +} + +interface Webview { + postMessage(msg: any): Thenable | boolean; +} + +function chunk(view: Webview | undefined, value: string) { + view?.postMessage({ type: 'streamChunk', value }); +} + +/** + * 슬래시 명령을 라우팅. chatHandlers에서 `userPrompt.startsWith('/')` 체크 후 호출. + * 처리 성공/실패 모두 true 반환 (사용자가 명령을 의도했음을 명확히 표현했으므로 + * LLM fallback로 흘리지 않음). + * + * **반드시 finally에서 `streamEnd`를 보낸다** — Astra webview의 채팅 input은 + * `streamEnd` 메시지를 받아야 잠금이 풀린다(sidebarProvider.ts 참조). 일반 LLM + * 흐름은 streamer가 자동으로 보내지만, 우리는 LLM을 우회해 bridge를 직접 호출 + * 하므로 명시적으로 보내야 한다. 안 보내면 timeout/에러/성공 어떤 경우에도 + * input이 영원히 잠긴 채 사용자가 무한 로딩 상태로 보게 됨. + */ +export async function handleSlashCommand( + input: string, + view: Webview | undefined, +): Promise { + const trimmed = input.trim(); + const spaceIdx = trimmed.indexOf(' '); + const head = (spaceIdx === -1 ? trimmed : trimmed.slice(0, spaceIdx)).toLowerCase() as SlashCommand; + const arg = spaceIdx === -1 ? '' : trimmed.slice(spaceIdx + 1).trim(); + + chunk(view, `\n\n**📻 Datacollect Radio** · \`${head}\` · bridge=\`${getBridgeBaseUrl()}\`\n\n`); + + try { + switch (head) { + case '/research': return await runResearch(arg, view); + case '/benchmark': return await runBenchmark(arg, view); + case '/youtube': return await runYoutube(arg, view); + case '/blog': return await runBlog(arg, view); + } + return true; + } catch (e: any) { + chunk(view, `\n\n> ❌ **에러**: ${e?.message || String(e)}\n`); + return true; + } finally { + // input 잠금 해제 — slashRouter 진입했으면 어떤 경로든 반드시 통과. + view?.postMessage({ type: 'streamEnd' }); + } +} + +// ───────────────────────────── /research ───────────────────────────── + +async function runResearch(topic: string, view: Webview | undefined): Promise { + if (!topic) { + chunk(view, `사용법: \`/research <주제>\`\n주제를 입력해 NotebookLM Deep Research를 시작합니다.\n`); + return true; + } + + chunk(view, `🚀 **Research 시작**: \`${topic}\`\n\n`); + const start = await bridgeFetch<{ success: boolean; notebookId: string; taskId: string }>( + '/api/research/start', + { method: 'POST', body: JSON.stringify({ topic }) }, + { timeoutMs: 60_000 }, + ); + chunk(view, `- notebookId: \`${start.notebookId}\`\n- taskId: \`${start.taskId}\`\n\n⏳ 상태 polling (5초 간격, 최대 10분)…\n`); + + // Deep research는 보통 1~5분. 5초 polling, 최대 120회(10분). + const deadline = Date.now() + 10 * 60_000; + let lastStatus = ''; + while (Date.now() < deadline) { + await new Promise(r => setTimeout(r, 5_000)); + // status 한 번 호출이 30s를 넘는 사례(stale MCP 자식)가 보고돼 60s로 완화. + const st = await bridgeFetch<{ success: boolean; result: any }>( + `/api/research/status?notebookId=${encodeURIComponent(start.notebookId)}&taskId=${encodeURIComponent(start.taskId)}`, + { method: 'GET' }, + { timeoutMs: 60_000 }, + ); + const status = String(st.result?.status || st.result || '').toLowerCase(); + if (status && status !== lastStatus) { + chunk(view, ` · ${status}\n`); + lastStatus = status; + } + if (status === 'completed' || status === 'done' || status === 'success' || status === 'finished') break; + if (status === 'failed' || status === 'error') { + chunk(view, `\n❌ Research 실패: ${JSON.stringify(st.result).slice(0, 400)}\n`); + return true; + } + } + + chunk(view, `\n📥 import…\n`); + // import는 deep research 결과를 노트북 소스로 옮기는 단계. 큰 리포트는 2~5분 + // 걸리는 경우가 흔해 120s에서 TRANSIENT_TIMEOUT으로 떨어지는 사례 보고됨. 300s로 늘림. + await bridgeFetch('/api/research/import', { + method: 'POST', + body: JSON.stringify({ notebookId: start.notebookId, taskId: start.taskId }), + }, { timeoutMs: 300_000 }); + + chunk(view, `🧪 synthesize…\n\n`); + // synthesize는 LLM이 노트북 전체를 합성 — 큰 노트북은 5~10분. 600s로 cap. + const synth = await bridgeFetch<{ success: boolean; markdown?: string; result?: string }>( + '/api/research/synthesize', + { + method: 'POST', + body: JSON.stringify({ notebookId: start.notebookId, topic, rootTopic: topic, includeKnowledgeConnections: true }), + }, + { timeoutMs: 600_000 }, + ); + const md = synth.markdown || synth.result || '(빈 응답)'; + chunk(view, `---\n\n${md}\n`); + return true; +} + +// ───────────────────────────── /benchmark ───────────────────────────── + +async function runBenchmark(url: string, view: Webview | undefined): Promise { + if (!url) { + chunk(view, `사용법: \`/benchmark \`\n예: \`/benchmark https://example.com\`\n`); + return true; + } + + chunk(view, `🔍 **Web Benchmark 스캔**: ${url}\n(Playwright + 디자인 토큰/사이트맵 추출, 최대 8페이지)\n\n`); + const scan = await bridgeFetch<{ success: boolean; scan: any; slug?: string }>( + '/api/web-benchmark/scan', + { + method: 'POST', + body: JSON.stringify({ url, captureScreenshots: false, maxPages: 8, crawlDepth: 1 }), + }, + { timeoutMs: 6 * 60_000 }, + ); + + const s = scan.scan; + chunk(view, `### 메타\n`); + chunk(view, `- **title**: ${s?.meta?.title || '(없음)'}\n`); + chunk(view, `- **description**: ${s?.meta?.description || '(없음)'}\n`); + chunk(view, `- **lang**: ${s?.meta?.lang || '(없음)'}\n\n`); + + chunk(view, `### 디자인 토큰 (상위)\n`); + const palette = s?.design?.colors?.palette?.slice(0, 5) || []; + chunk(view, `- **컬러 팔레트 Top 5**: ${palette.map((p: any) => `\`${p.value}\` (×${p.count})`).join(', ') || '(없음)'}\n`); + chunk(view, `- **컬러 비율**: ${s?.design?.colors?.composition?.ratioLabel || '(없음)'}\n`); + chunk(view, `- **Primary Font**: \`${s?.design?.typography?.primaryFont || '(없음)'}\`\n`); + chunk(view, `- **그리드**: ${(s?.design?.layout?.grids || []).map((g: any) => g.columnsRaw).join(' | ') || '(없음)'}\n\n`); + + chunk(view, `### 사이트맵 (${s?.sitemap?.totalPages ?? 1}페이지, depth ${s?.sitemap?.crawlDepth ?? 0})\n`); + chunk(view, `\`\`\`\n${s?.sitemap?.ascii || '(없음)'}\n\`\`\`\n\n`); + + chunk(view, `### 마이크로카피\n`); + chunk(view, `- **헤드라인**: ${s?.microcopy?.headline || '(없음)'}\n`); + chunk(view, `- **CTA Top 5**: ${(s?.microcopy?.ctaSamples || []).slice(0, 5).map((c: string) => `\`${c}\``).join(', ') || '(없음)'}\n\n`); + + chunk(view, `> 💡 더 깊은 4-렌즈/Rebuild Blueprint 합성을 원하면 위 결과를 인용해 Astra에 추가 질문하세요.\n`); + return true; +} + +// ───────────────────────────── /youtube ───────────────────────────── + +async function runYoutube(url: string, view: Webview | undefined): Promise { + if (!url) { + chunk(view, `사용법: \`/youtube \`\n예: \`/youtube https://youtu.be/xxxx\`\n`); + return true; + } + + chunk(view, `🎬 **YouTube 추출**: ${url}\n(transcript + metadata)\n\n`); + const data = await bridgeFetch<{ success: boolean; metadata?: any; segments?: any[]; plainTranscript?: string; outputDir?: string }>( + '/api/youtube/extract', + { method: 'POST', body: JSON.stringify({ url }) }, + { timeoutMs: 5 * 60_000 }, + ); + + const m = data.metadata || {}; + chunk(view, `### 메타데이터\n`); + chunk(view, `- **title**: ${m.title || '(없음)'}\n`); + chunk(view, `- **channel**: ${m.channel || m.uploader || '(없음)'}\n`); + chunk(view, `- **duration**: ${m.duration || '(없음)'}초\n`); + chunk(view, `- **views**: ${m.view_count ?? '(없음)'}\n`); + chunk(view, `- **upload_date**: ${m.upload_date || '(없음)'}\n`); + if (Array.isArray(m.tags) && m.tags.length) { + chunk(view, `- **tags**: ${m.tags.slice(0, 10).join(', ')}\n`); + } + if (Array.isArray(m.chapters) && m.chapters.length) { + chunk(view, `\n### 챕터 (${m.chapters.length})\n`); + for (const c of m.chapters.slice(0, 12)) { + chunk(view, `- ${c.start_time}s — ${c.title}\n`); + } + } + + const transcript = data.plainTranscript || ''; + chunk(view, `\n### Transcript (${transcript.length.toLocaleString()}자, 처음 4000자만 미리보기)\n\n`); + chunk(view, `\`\`\`\n${transcript.slice(0, 4000)}${transcript.length > 4000 ? '\n... (생략)' : ''}\n\`\`\`\n`); + + chunk(view, `\n> 💡 Hook/Structure/Production/CTR 4-렌즈 분석을 원하면 위 transcript를 인용해 Astra에 추가 질문하세요.\n`); + return true; +} + +// ───────────────────────────── /blog ───────────────────────────── + +async function runBlog(keyword: string, view: Webview | undefined): Promise { + // Blog Pipeline은 Datacollect의 별도 흐름(blog/app.js + local_platform_server 8787)으로 + // 실행된다. Bridge 3002에는 대응 endpoint가 없어 Astra가 직접 호출할 경로가 없음. + // MVP에서는 사용자가 그쪽 UI로 빠르게 갈 수 있도록 안내만. + const target = 'http://127.0.0.1:8787/blog/'; + chunk(view, `🖋️ **Blog Pipeline**\n\n`); + if (keyword) { + chunk(view, `요청 키워드: \`${keyword}\`\n\n`); + } + chunk(view, [ + `현재 Blog Pipeline은 Datacollect의 별도 정적 페이지(`, + `[${target}](${target})`, + `)에서 단계별로 실행됩니다. Bridge(3002)에 대응 API가 없어 채팅에서 직접`, + ` 트리거할 수 없어, 다음 두 가지 중 하나를 선택해 주세요:\n\n`, + ].join('')); + chunk(view, `1. 위 링크에서 키워드/참고 자료/경험담을 입력해 직접 파이프라인 실행\n`); + chunk(view, `2. Bridge에 \`/api/blog/run\` 같은 endpoint를 노출하면 \`/blog\`도 end-to-end 실행 가능 — 원하시면 추가 작업으로 진행\n`); + + try { + await vscode.env.openExternal(vscode.Uri.parse(target)); + } catch { /* best-effort */ } + return true; +} diff --git a/src/sidebar/chatHandlers.ts b/src/sidebar/chatHandlers.ts index 153928d..d746116 100644 --- a/src/sidebar/chatHandlers.ts +++ b/src/sidebar/chatHandlers.ts @@ -18,6 +18,17 @@ export async function handleChatMessage(provider: SidebarChatProvider, data: any case 'promptWithFile': provider._lmStudio?.activity.bump(); await provider._context.globalState.update(SidebarChatProvider.blankChatStateKey, false); + // ── 📻 Datacollect Radio (slash 명령) 우선 분기 ── + // 사용자가 채팅에서 `/research`, `/benchmark`, `/youtube`, `/blog` 같은 + // 슬래시 명령을 보내면 Datacollect bridge(3002)로 위임. 회사 모드/일반 + // chat 분기보다 먼저 잡아 LLM 토큰을 쓰지 않고 직접 처리한다. + if (typeof data.value === 'string') { + const { isSlashCommand, handleSlashCommand } = await import('../features/datacollect/slashRouter'); + if (isSlashCommand(data.value)) { + await handleSlashCommand(data.value, provider._view?.webview); + return true; + } + } // ── 1인 기업 모드 우선 분기 ── // 회사 모드일 땐 들어온 메시지를 intent classifier에 한 번 통과시켜 // (a) 잡담/질문/짧은 응답 → 일반 채팅 경로, (b) 후속 대화 → 일반 채팅