From 9e7c7fe605d5da5b6b120d1f8d6a1dfe12bbea29 Mon Sep 17 00:00:00 2001 From: g1nation Date: Wed, 20 May 2026 13:22:26 +0900 Subject: [PATCH] Astra v2.2.41: /benchmark LLM 4-lens synthesis + Datacollect settings - /benchmark now runs the full scan -> LLM 3-stage 4-lens synthesis -> markdown report pipeline, matching the Datacollect web app output - Add settings: datacollectSynthesisTemperature (0.1), datacollectCrawlDepth, datacollectMaxPages, datacollectSavePath; new "Datacollect" Settings section - Fix slash result not rendering (missing streamStart) and /benchmark URL parsing when natural language is appended - Rename view container/view ids to g1nation-* to avoid conflict with the Antigravity built-in "Connect AI" extension - Version bump 2.2.34 -> 2.2.41 Co-Authored-By: Claude Opus 4.7 (1M context) --- ...d46d2ca2057b05c488be1dcf439166ac5a9a1.json | 2 +- ...9f4f39d2bc368f77456c37b5eef9a94a66b5c.json | 2 +- ...5c7a44d7661af673b24e3f49551a7a2e50280.json | 2 +- ...adc543795e4b427b64540a49c9ab27c7fe213.json | 4 +- ...son => stress_conflict_1779250764052.json} | 18 +- PATCHNOTES.md | 71 +++ media/settings-panel.html | 45 ++ media/settings-panel.js | 34 ++ package.json | 40 +- src/extension.ts | 10 +- src/features/datacollect/slashRouter.ts | 441 +++++++++++++++++- .../settings/settingsPanelProvider.ts | 40 ++ src/sidebar/chatHandlers.ts | 16 +- 13 files changed, 666 insertions(+), 59 deletions(-) rename .astra/tests/stress/.astra/missions/{stress_conflict_1779186935983.json => stress_conflict_1779250764052.json} (81%) diff --git a/.astra/tests/stress/.astra/cache/259a37934ead3910a8722b82054d46d2ca2057b05c488be1dcf439166ac5a9a1.json b/.astra/tests/stress/.astra/cache/259a37934ead3910a8722b82054d46d2ca2057b05c488be1dcf439166ac5a9a1.json index aa93fb2..dfb63e9 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": 1779186936004, + "createdAt": 1779250764072, "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 532a5a4..4494ade 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": 1779186936000, + "createdAt": 1779250764070, "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 0aa5fd1..e9eacb7 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": 1779186935997, + "createdAt": 1779250764068, "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 cf21064..f491b4b 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_1779186935983\ndate: 2026-05-19T10:35:36.005Z\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]** 핵심 정보 수집 및 분석 중... (3ms)\n- **[WRITER]** 최종 리포트 작성 및 편집 중... (4ms)\n", - "createdAt": 1779186936005, + "result": "---\nid: stress_conflict_1779250764052\ndate: 2026-05-20T04:19:24.073Z\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]** 핵심 정보 수집 및 분석 중... (3ms)\n- **[WRITER]** 최종 리포트 작성 및 편집 중... (2ms)\n", + "createdAt": 1779250764073, "modelVersion": "unknown" } \ No newline at end of file diff --git a/.astra/tests/stress/.astra/missions/stress_conflict_1779186935983.json b/.astra/tests/stress/.astra/missions/stress_conflict_1779250764052.json similarity index 81% rename from .astra/tests/stress/.astra/missions/stress_conflict_1779186935983.json rename to .astra/tests/stress/.astra/missions/stress_conflict_1779250764052.json index 0f1ce26..2dd2896 100644 --- a/.astra/tests/stress/.astra/missions/stress_conflict_1779186935983.json +++ b/.astra/tests/stress/.astra/missions/stress_conflict_1779250764052.json @@ -1,8 +1,8 @@ { - "missionId": "stress_conflict_1779186935983", + "missionId": "stress_conflict_1779250764052", "status": "completed", - "startTime": "2026-05-19T10:35:35.983Z", - "totalElapsedMs": 24, + "startTime": "2026-05-20T04:19:24.052Z", + "totalElapsedMs": 22, "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-19T10:35:35.996Z" + "ts": "2026-05-20T04:19:24.066Z" }, { "from": "planner", "to": "researcher", "durationMs": 3, "message": "핵심 정보 수집 및 분석 중...", - "ts": "2026-05-19T10:35:35.999Z" + "ts": "2026-05-20T04:19:24.069Z" }, { "from": "researcher", "to": "writer", - "durationMs": 4, + "durationMs": 2, "message": "최종 리포트 작성 및 편집 중...", - "ts": "2026-05-19T10:35:36.003Z" + "ts": "2026-05-20T04:19:24.071Z" }, { "from": "writer", "to": "completed", "durationMs": 3, "message": "미션 완료", - "ts": "2026-05-19T10:35:36.006Z" + "ts": "2026-05-20T04:19:24.074Z" } ], "resilienceMetrics": { diff --git a/PATCHNOTES.md b/PATCHNOTES.md index 25eef50..156a67c 100644 --- a/PATCHNOTES.md +++ b/PATCHNOTES.md @@ -1,5 +1,76 @@ # Astra Patch Notes +## v2.2.41 (2026-05-20) +### 🎛️ /benchmark 합성 Temperature 설정 추가 +- **`g1nation.datacollectSynthesisTemperature` 신설** (기본 0.1). `/benchmark` LLM 4-렌즈 합성의 temperature를 Astra Settings 패널 'Datacollect' 섹션에서 조절 가능 — 그동안 코드에 `0.3`으로 하드코딩돼 있었다. 낮출수록(0.1) 한국어 생성 중 섞이는 깨진 문자·환각이 줄고 결과가 결정적이다. 0~2 범위로 클램프. +- **신규 패키징:** `astra-2.2.41.vsix`. + +--- + + + +## v2.2.40 (2026-05-20) +### 📦 재패키징 (코드 변경 없음) +- v2.2.39와 코드·기능 완전 동일. 버전 번호만 갱신한 재패키징 빌드 — v2.2.39의 변경사항(/benchmark LLM 4-렌즈 합성, 크롤 깊이/페이지 설정)이 모두 그대로 포함된다. +- **신규 패키징:** `astra-2.2.40.vsix`. + +--- + + + +## v2.2.39 (2026-05-20) +### ✨ /benchmark LLM 4-렌즈 합성 + 크롤 깊이/페이지 설정 +- **LLM 4-렌즈 합성 추가 — 결과물이 Datacollect 웹앱과 동등해짐.** 그동안 `/benchmark`는 Playwright 스캔 데이터를 그대로 덤프만 했다(디자인 토큰 raw 목록). 이제 Datacollect 웹앱(WebBenchmarkPanel)과 동일하게 `scan → LLM 3단계 합성(Visual/Layout/Interaction/Voice 4-렌즈 + IA·페이지 템플릿 + 원본 재구축 명세) → 완성된 마크다운 리포트`를 생성한다. Bridge `/api/lm` 프록시 경유, Astra 기본 모델(`g1nation.defaultModel`/`ollamaUrl`)을 사용. 합성 실패 시 raw 스캔 요약으로 자동 fallback. +- **크롤 깊이·최대 페이지 설정 추가.** `g1nation.datacollectCrawlDepth`(기본 1)·`g1nation.datacollectMaxPages`(기본 8) 신설 — Astra Settings 패널 'Datacollect' 섹션에서 편집. 명령어에서 `/benchmark depth=2 pages=12` 로 그때그때 덮어쓰기 가능 (우선순위: 명령어 > 설정 > 기본값). +- **명령어 보조 컨텍스트.** `/benchmark [depth=N] [pages=N] <자연어 설명>` 형태로 URL 뒤에 붙인 자연어는 합성 part 1·2의 톤 추정 보조 컨텍스트로 전달된다. +- **신규 패키징:** `astra-2.2.39.vsix`. + +--- + + + +## v2.2.38 (2026-05-20) +### 🐛 /benchmark URL 파싱 fix — 자연어 섞인 입력 + 빈 결과 경고 +- **URL 토큰만 추출.** `/benchmark www.caliverse.io 분석해줘` 처럼 URL 뒤에 자연어를 붙이면 "분석해줘"까지 URL에 섞여 `https://www.caliverse.io 분석해줘.` 라는 무효 URL이 되고, Playwright가 페이지를 못 열어 meta·디자인·마이크로카피가 전부 `(없음)`으로 나오던 문제. 이제 arg의 첫 공백 전 토큰만 URL로 사용한다. (스캔은 Playwright 결정론적 추출이라 local LLM과 무관 — 빈 결과는 LLM 문제가 아니라 URL 오염이었다.) +- **빈 스캔 결과 경고.** 스캔이 에러 없이 끝나도 meta·design·microcopy가 모두 비면 ⚠️ 경고를 표시 — URL 오타·봇 차단·JS 전용 렌더링을 사용자가 즉시 인지. +- **신규 패키징:** `astra-2.2.38.vsix`. + +--- + + + +## v2.2.37 (2026-05-20) +### ✅ Slash 결과물 미표시 근본 fix + /benchmark 진행 표시·결과물 저장 +- **`streamStart` 누락 수정 — slash 명령 결과가 화면에 안 보이던 진짜 원인.** v2.2.30이 `streamEnd`(input 잠금 해제)만 보냈고 `streamStart`는 빠뜨렸다. webview는 `streamStart`를 받아야 assistant 메시지 버블(`streamBody`)을 만들고, 그게 없으면 이후 모든 `streamChunk`(=결과 텍스트, 에러 메시지 포함)를 silently drop한다. `handleSlashCommand` 시작부에서 `streamStart`를 명시적으로 보내도록 수정 — `/research`·`/benchmark`·`/youtube`·`/blog` 모두 결과가 정상 표시됨. +- **`/benchmark` 진행 표시.** scan은 단일 동기 호출이라 진행률 스트림이 없어 사용자가 멈춘 줄 오해하던 문제 → 4초마다 경과 시간(`·4s ·8s …`)을 한 줄에 누적 표시하고, 완료 시 `✅ 스캔 완료 (Ns 소요)` 출력. +- **`/benchmark` 결과물 자동 저장.** scan 결과를 markdown 리포트로 합성해 Bridge `/api/wiki/save`로 저장. 저장 위치는 신규 설정 `g1nation.datacollectSavePath`(비우면 Bridge의 `WIKI_RAW_PATH` 환경변수가 결정 — 코드에 절대경로 하드코딩 없음). +- **Settings 패널에 "Datacollect" 섹션 추가.** Bridge URL과 결과물 저장 폴더를 Astra Settings 패널에서 직접 편집 가능. +- **신규 패키징:** `astra-2.2.37.vsix`. + +--- + + + +## v2.2.36 (2026-05-20) +### 🛠️ Antigravity 빌트인 "Connect AI"와의 view 충돌 해소 +- **사용자 보고 console에 `Connect AI extension activated` + 우리 `[ASTRA-DEBUG]` 로그 0개** → 우리 vsix activate 함수가 호출조차 안 되고 있다는 결정적 증거. Antigravity가 빌트인으로 가진 "Connect AI" extension이 우리 view container id `astra-activity` / view id `astra-launcher` / title `Astra` 영역과 충돌해 우리 코드를 덮은 것으로 추정. +- **view container/view id를 unique하게 변경**: `astra-activity` → `g1nation-astra-activity`, `astra-launcher` → `g1nation-astra-launcher`, title `Astra` → `Astra (g1nation)`. 좌측 활성바에 빌트인과 별도의 우리 아이콘이 표시되며, 그 아이콘 클릭 시 우리 activate가 호출됨. +- **신규 패키징:** `astra-2.2.36.vsix`. + +--- + + + +## v2.2.35 (2026-05-20) +### 🛠️ Slash 미동작 결정적 fix: globalState 이전 진입 + DevTools console 진단 +- **globalState write보다 *먼저* slash 분기를 잡도록 변경.** `g1nation.astra` global state가 ~917KB로 누적된 환경에서 `globalState.update` 호출이 첫 prompt를 지연/hang시키는 사례 보고됨. slash 명령은 LLM/blank-chat state 갱신과 무관하므로 update를 건너뛴다. +- **DevTools console에 ASTRA-DEBUG trace.** OutputChannel(`logInfo`)·popup·statusbar에 더해 `console.error`로 `[ASTRA-DEBUG] activate vX.Y.Z`, `[ASTRA-DEBUG] prompt case entered`, `[ASTRA-DEBUG] slash check matched=…`, `[ASTRA-DEBUG] slashRouter handleSlashCommand`을 출력. 사용자가 F12 DevTools console을 볼 때 우리 코드 실행 단계가 즉시 보임. +- **신규 패키징:** `astra-2.2.35.vsix`. + +--- + + + ## v2.2.34 (2026-05-19) ### 🚨 결정적 진단: activate / slash 진입 popup notification - **Activation 시점 popup:** VS Code/Antigravity가 시작될 때 화면 우측 하단에 `📡 Astra vX.Y.Z activated (PID=...)` 알림. 우리 vsix가 실제로 활성화됐는지 1초 안에 확인 가능. 같은 이름의 빌트인 extension이 우리 코드를 가리는 케이스 발견용. diff --git a/media/settings-panel.html b/media/settings-panel.html index de6e54c..ce3caba 100644 --- a/media/settings-panel.html +++ b/media/settings-panel.html @@ -45,6 +45,51 @@ + +
+

Datacollect (slash 명령)

+

채팅에서 /research · /benchmark · /youtube 를 입력하면 Datacollect Bridge로 위임됩니다. Bridge는 Datacollect 프로젝트에서 npm run bridge 로 실행해야 합니다.

+
+ +
+ + +
+
+
+ +
+ + +
+ /benchmark 등의 결과 markdown 저장 위치. 비워두면 Bridge의 WIKI_RAW_PATH 환경변수가 결정합니다 (코드에 절대경로 하드코딩 없음). 특정 폴더로 저장하려면 절대경로를 입력하세요. +
+
+ +
+ + +
+ /benchmark 사이트맵 크롤 깊이. 0=루트만, 1=직속 자식, 2=손자, 3=깊은 크롤. 명령어에서 depth=N 으로 그때그때 덮어쓸 수 있습니다. +
+
+ +
+ + +
+ /benchmark 스캔 최대 페이지 수. 명령어에서 pages=N 으로 덮어쓸 수 있습니다 (Bridge 상한 20). +
+
+ +
+ + +
+ /benchmark LLM 4-렌즈 합성의 temperature. 낮을수록(0.1) 환각·깨진 문자가 줄고 결정적입니다. 기본 0.1 권장. +
+
+

메모리

diff --git a/media/settings-panel.js b/media/settings-panel.js index d48ea3e..dbe0f25 100644 --- a/media/settings-panel.js +++ b/media/settings-panel.js @@ -24,6 +24,13 @@ const cnRefreshModels = $('cnRefreshModels'); const cnModelHint = $('cnModelHint'); + // ---- Datacollect ---- + const dcBridgeUrl = $('dcBridgeUrl'); + const dcSavePath = $('dcSavePath'); + const dcCrawlDepth = $('dcCrawlDepth'); + const dcMaxPages = $('dcMaxPages'); + const dcSynthTemp = $('dcSynthTemp'); + // ---- Memory ---- const memEnabled = $('memEnabled'); const memShort = $('memShort'); @@ -113,6 +120,23 @@ vscode.postMessage({ type: 'connection.update', requestTimeout: Number(cnTimeout.value) }) ); + // ---- Datacollect listeners ---- + document.querySelector('[data-save="datacollect.bridgeUrl"]').addEventListener('click', () => + vscode.postMessage({ type: 'datacollect.update', bridgeUrl: dcBridgeUrl.value }) + ); + document.querySelector('[data-save="datacollect.savePath"]').addEventListener('click', () => + vscode.postMessage({ type: 'datacollect.update', savePath: dcSavePath.value }) + ); + document.querySelector('[data-save="datacollect.crawlDepth"]').addEventListener('click', () => + vscode.postMessage({ type: 'datacollect.update', crawlDepth: Number(dcCrawlDepth.value) }) + ); + document.querySelector('[data-save="datacollect.maxPages"]').addEventListener('click', () => + vscode.postMessage({ type: 'datacollect.update', maxPages: Number(dcMaxPages.value) }) + ); + document.querySelector('[data-save="datacollect.synthesisTemperature"]').addEventListener('click', () => + vscode.postMessage({ type: 'datacollect.update', synthesisTemperature: Number(dcSynthTemp.value) }) + ); + // ---- Memory listeners ---- memEnabled.addEventListener('change', (e) => vscode.postMessage({ type: 'memory.update', memoryEnabled: e.target.checked }) @@ -338,6 +362,16 @@ ? '모델 목록 가져오는 중…' : `사이드바에서 선택한 모델이 여기에도 동기화됩니다. (${list.length}개 발견)`; + // ---- Datacollect ---- + const dc = state.datacollect; + if (dc) { + setIfNotFocused(dcBridgeUrl, dc.bridgeUrl); + setIfNotFocused(dcSavePath, dc.savePath); + setIfNotFocused(dcCrawlDepth, dc.crawlDepth); + setIfNotFocused(dcMaxPages, dc.maxPages); + setIfNotFocused(dcSynthTemp, dc.synthesisTemperature); + } + // ---- Memory ---- const mem = state.memory; memEnabled.checked = !!mem.memoryEnabled; diff --git a/package.json b/package.json index 7f0495a..9335338 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.34", + "version": "2.2.41", "publisher": "g1nation", "license": "MIT", "icon": "assets/icon.png", @@ -171,23 +171,23 @@ "viewsContainers": { "activitybar": [ { - "id": "astra-activity", - "title": "Astra", + "id": "g1nation-astra-activity", + "title": "Astra (g1nation)", "icon": "assets/icon-activitybar.svg" } ] }, "views": { - "astra-activity": [ + "g1nation-astra-activity": [ { - "id": "astra-launcher", - "name": "Astra Launcher" + "id": "g1nation-astra-launcher", + "name": "Astra (g1nation) Launcher" } ] }, "viewsWelcome": [ { - "view": "astra-launcher", + "view": "g1nation-astra-launcher", "contents": "✦ **Astra** — 로컬 AI 인텔리전스 레이어\n\nChat 탭을 닫았을 때 여기서 다시 열 수 있습니다.\n\n[$(comment-discussion) Open Chat](command:g1nation.openChat)\n[$(add) New Chat](command:g1nation.newChat)\n[$(gear) Settings](command:g1nation.settings.focus)\n\n---\n\n**1인 기업 모드**\n\n[$(organization) Manage Agents](command:g1nation.company.manage)\n[$(folder-opened) Open Sessions Folder](command:g1nation.company.openSessions)\n\n---\n\n**Project Architecture**\n\n[$(file-text) Open Architecture Doc](command:g1nation.architecture.open)\n[$(refresh) Refresh Architecture](command:g1nation.architecture.refresh)\n\n---\n\n**Lessons / Knowledge**\n\n[$(lightbulb) Manage Lessons](command:g1nation.lesson.manage)\n[$(edit) Edit Agent ↔ Knowledge Map](command:g1nation.skills.editKnowledgeMap)" } ], @@ -204,6 +204,32 @@ "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.datacollectSavePath": { + "type": "string", + "default": "", + "markdownDescription": "`/benchmark` 등 Datacollect slash 명령 결과물(markdown)을 저장할 폴더. **비워두면** Bridge 기본 위치(Bridge의 `WIKI_RAW_PATH` 환경변수)에 저장됩니다 — 코드/설정 어디에도 절대경로가 박히지 않습니다. 특정 폴더로 저장하려면 절대경로를 입력하세요. Astra Settings 패널의 'Datacollect' 섹션에서도 편집 가능." + }, + "g1nation.datacollectCrawlDepth": { + "type": "number", + "default": 1, + "minimum": 0, + "maximum": 3, + "markdownDescription": "`/benchmark` 사이트맵 크롤 깊이 기본값. 0=루트만, 1=직속 자식, 2=손자, 3=깊은 크롤. 명령어에서 `depth=N`으로 그때그때 덮어쓸 수 있습니다. (단일 페이지 SPA는 깊이를 올려도 1페이지)" + }, + "g1nation.datacollectMaxPages": { + "type": "number", + "default": 8, + "minimum": 1, + "maximum": 20, + "markdownDescription": "`/benchmark` 스캔 최대 페이지 수 기본값. 명령어에서 `pages=N`으로 덮어쓸 수 있습니다. Bridge에서 20으로 상한이 적용됩니다." + }, + "g1nation.datacollectSynthesisTemperature": { + "type": "number", + "default": 0.1, + "minimum": 0, + "maximum": 2, + "markdownDescription": "`/benchmark` LLM 4-렌즈 합성의 temperature. 낮을수록(0.1) 환각·깨진 문자가 줄고 결과가 결정적이며, 높을수록 표현이 다양해집니다. 기본 0.1 권장." + }, "g1nation.memoryEnabled": { "type": "boolean", "default": true, diff --git a/src/extension.ts b/src/extension.ts index ca3ae21..98338e9 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -49,11 +49,13 @@ const TELEGRAM_TOKEN_SECRET_KEY = 'g1nation.telegram.botToken'; * Astra Extension Entry Point */ export async function activate(context: vscode.ExtensionContext) { - // Activation 시점 popup — 사용자 환경(Antigravity 등 VS Code 변종)에서 우리 vsix가 - // 실제로 활성화됐는지 결정적으로 가시화. 같은 이름의 빌트인이 우선해 우리 코드가 - // 활성화 안 되는 케이스를 즉시 발견 가능. + // Activation 시점 popup + DevTools console — 사용자 환경(Antigravity 등 VS Code 변종) + // 에서 우리 vsix가 실제로 활성화됐는지 결정적으로 가시화. console.error는 사용자가 + // F12 DevTools console에서 다른 모든 출력과 함께 그대로 보인다 (logInfo의 OutputChannel + // 과 별개 채널 — popup도 OutputChannel도 못 보는 경우의 마지막 안전망). const ext = vscode.extensions.getExtension('g1nation.astra'); const version = ext?.packageJSON?.version || '(unknown)'; + console.error(`[ASTRA-DEBUG] activate v${version} pid=${process.pid}`); void vscode.window.showInformationMessage(`📡 Astra v${version} activated (PID=${process.pid})`); logInfo(`Astra activating... version=${version} pid=${process.pid}`); @@ -184,7 +186,7 @@ export async function activate(context: vscode.ExtensionContext) { getChildren: () => [], }; context.subscriptions.push( - vscode.window.registerTreeDataProvider('astra-launcher', astraLauncherProvider), + vscode.window.registerTreeDataProvider('g1nation-astra-launcher', astraLauncherProvider), ); // 4. Initialize Bridge Server (Port 4825) diff --git a/src/features/datacollect/slashRouter.ts b/src/features/datacollect/slashRouter.ts index 2cabdb8..a9181ed 100644 --- a/src/features/datacollect/slashRouter.ts +++ b/src/features/datacollect/slashRouter.ts @@ -54,12 +54,16 @@ export async function handleSlashCommand( const head = (spaceIdx === -1 ? trimmed : trimmed.slice(0, spaceIdx)).toLowerCase() as SlashCommand; const arg = spaceIdx === -1 ? '' : trimmed.slice(spaceIdx + 1).trim(); + console.error(`[ASTRA-DEBUG] slashRouter handleSlashCommand head=${head} arg=${arg.slice(0, 40)}`); logInfo(`[SLASH] handleSlashCommand start head=${head} arg="${arg.slice(0, 60)}" bridge=${getBridgeBaseUrl()}`); - // 사용자가 OutputChannel을 안 봐도 진입 사실을 확인할 수 있도록 popup + statusbar. - // chunk(view, ...)가 webview 비활성 등으로 silently drop돼도 popup은 화면 우측 하단에 - // 큼지막하게 표시된다. setStatusBarMessage는 보조. void vscode.window.showInformationMessage(`📻 Datacollect Radio: ${head} 진입`); void vscode.window.setStatusBarMessage(`📻 Datacollect Radio: ${head} 처리 중…`, 5000); + // streamEnd(finally)와 대칭 — webview는 streamStart를 받아야 assistant 메시지 + // 버블(streamBody)을 만든다(sidebar.js 참조). 이게 없으면 이후 chunk가 보내는 + // streamChunk가 streamBody 부재로 전부 silently drop돼 사용자는 아무 결과도 + // 못 본다. 일반 LLM 경로는 streamer가 streamStart를 보내주지만, slash 경로는 + // LLM을 우회하므로 streamEnd처럼 streamStart도 명시적으로 보내야 한다. + view?.postMessage({ type: 'streamStart' }); chunk(view, `\n\n**📻 Datacollect Radio** · \`${head}\` · bridge=\`${getBridgeBaseUrl()}\`\n\n`); try { @@ -145,43 +149,432 @@ async function runResearch(topic: string, view: Webview | undefined): Promise { +type SynthesisPart = 1 | 2 | 3; + +/** + * scan JSON → 4-렌즈 분석 LLM 프롬프트. Datacollect 웹앱(WebBenchmarkPanel)의 + * buildSynthesisPrompt를 그대로 이식 — /benchmark 결과가 웹앱과 동등하게 나오도록. + */ +function buildSynthesisPrompt(scan: any, userContent: string, part: SynthesisPart): string { + // 4-렌즈 분석에 필요한 핵심 데이터만 추려 LLM 입력을 가볍게. + const slim = { + url: scan?.url, + title: scan?.meta?.title, + description: scan?.meta?.description, + lang: scan?.meta?.lang, + + // §1. 비주얼 아이덴티티 — 컬러 비율 + 다크모드 + 타이포 위계 + colors: { + palette: scan?.design?.colors?.palette?.slice(0, 8), + composition: scan?.design?.colors?.composition, + background: scan?.design?.colors?.background, + primaryText: scan?.design?.colors?.primaryText, + linkColor: scan?.design?.colors?.linkColor, + buttonBackground: scan?.design?.colors?.buttonBackground, + buttonText: scan?.design?.colors?.buttonText, + darkModeHints: scan?.design?.colors?.darkModeHints, + }, + typography: { + primaryFont: scan?.design?.typography?.primaryFont, + fontStack: scan?.design?.typography?.fontStack?.slice(0, 3), + topFontSizes: scan?.design?.typography?.topFontSizes?.slice(0, 6), + topFontWeights: scan?.design?.typography?.topFontWeights?.slice(0, 5), + body: scan?.design?.typography?.body, + h1: scan?.design?.typography?.h1, + h2: scan?.design?.typography?.h2, + h3: scan?.design?.typography?.h3, + button: scan?.design?.typography?.button, + }, + + // §2. 레이아웃 & 공간감 — 여백 / 그리드 + layout: { + viewport: { w: scan?.design?.layout?.viewportWidth, h: scan?.design?.layout?.viewportHeight }, + bodyMaxWidth: scan?.design?.layout?.bodyMaxWidth, + sectionSpacing: scan?.design?.layout?.sectionSpacing, + cardSpacing: scan?.design?.layout?.cardSpacing, + borderRadiusScale: scan?.design?.layout?.borderRadiusScale, + grids: scan?.design?.layout?.grids, + containerSystem: scan?.design?.layout?.containerSystem, + responsiveHints: scan?.design?.layout?.responsiveHints, + layering: scan?.design?.layout?.layering, + }, + components: scan?.design?.components, + mediaTreatment: scan?.design?.mediaTreatment, + surfaceTreatment: scan?.design?.surfaceTreatment, + + // §3. 마이크로 인터랙션 — Hover / Transition + interactions: { + hoverRules: scan?.interactions?.hoverRules?.slice(0, 6), + focusRules: scan?.interactions?.focusRules?.slice(0, 3), + transitionDistribution: scan?.interactions?.transitionDistribution, + cssVars: scan?.interactions?.cssVars, + }, + + // §4. 라이팅 톤앤매너 — 마이크로카피 + microcopy: { + headline: scan?.microcopy?.headline, + subheadline: scan?.microcopy?.subheadline, + subheadlines: scan?.microcopy?.subheadlines?.slice(0, 6), + ctaSamples: scan?.microcopy?.ctaSamples?.slice(0, 10), + placeholders: scan?.microcopy?.placeholders, + stateMessages: scan?.microcopy?.stateMessages, + ariaLabels: scan?.microcopy?.ariaLabels?.slice(0, 6), + bodySample: scan?.microcopy?.bodySample, + voiceSignals: scan?.microcopy?.voiceSignals, + }, + + // 구조 요약 — 역기획서 매칭을 위한 보조 정보 + structure: { + sections: scan?.structure?.sections?.slice(0, 10).map((s: any) => ({ + role: s.role, + depth: s.depth, + text: s.textPreview?.slice(0, 100), + btns: s.buttonCount, + links: s.linkCount, + imgs: s.imgCount, + })), + h1: scan?.structure?.h1, + h2List: scan?.structure?.h2List?.slice(0, 6), + navigationLinks: scan?.structure?.navigationLinks?.slice(0, 10), + }, + + iconography: scan?.design?.iconography, + + // §5. 사이트맵 — 준비할 리소스 추정의 근거 (페이지별 역할 + 자산 수). + sitemap: scan?.sitemap ? { + totalPages: scan.sitemap.totalPages, + crawlDepth: scan.sitemap.crawlDepth, + asciiTree: scan.sitemap.ascii, + pages: scan.sitemap.pages?.map((p: any) => ({ + url: p.url, + role: p.role, + title: p.title?.slice(0, 80), + h1: p.h1?.slice(0, 80), + h2List: p.h2List?.slice(0, 5), + contentType: p.primaryContentType, + imageCount: p.imageCount, + videoCount: p.videoCount, + formFields: p.formFields?.slice(0, 6).map((f: any) => ({ + name: f.name || f.label, type: f.type, required: f.required, + })), + ctas: p.ctaSamples?.slice(0, 4), + sections: p.sectionRoles?.slice(0, 8).map((s: any) => ({ + tag: s.tag, hint: s.hint, preview: (s.preview || '').slice(0, 50), + })), + error: p.error, + })), + } : null, + }; + + const today = new Date().toISOString().slice(0, 10); + const title = slim.title || 'Reference Site'; + const userBlock = userContent.trim() + ? userContent.trim() + : '(미입력 — 원본 사이트 자체의 재현에만 집중)'; + + const sharedRules = ` +[분석 원칙] +1. 이 보고서의 미션은 "원본 레퍼런스 사이트를 가능한 한 닮은 사이트를 처음부터 다시 만들기 위한 명세"를 작성하는 것이다. +2. 추측이나 일반론은 금지. 모든 진술은 제공된 JSON 스캔 데이터의 구체적 수치/문자열을 근거로 인용한다. +3. JSON에 없는 정보를 지어내지 말 것. 데이터에 없는 항목은 "스캔 데이터 부족"이라고 명시한다. +4. 한국어로 작성한다. +5. 모든 색상/폰트/여백/Radius는 정확한 값(rgb/px)을 그대로 인용한다.`; + + const commonHeader = ` +# ${title} 레퍼런스 사이트 재구축 명세 + +> **레퍼런스 URL**: ${slim.url} +> **분석 일자**: ${today} +> **분석 관점**: 4-렌즈 (Visual / Layout / Interaction / Voice) + IA 및 페이지 템플릿 + 재구축 명세 +> **스캔된 페이지**: ${slim.sitemap?.totalPages ?? 1}개 (crawlDepth: ${slim.sitemap?.crawlDepth ?? 0})`; + + const partTemplate = part === 1 + ? ` +${commonHeader} + +## 한 줄 요약 (One-line Impression) + +## 1. 시각적 정체성 (Visual Identity) +### 1-1. 컬러 팔레트 (Color Palette) +### 1-2. 타이포그래피 (Typography) + +## 2. 레이아웃 및 여백 (Layout & Whitespace) +### 2-1. 그리드 시스템 (Grid System) +### 2-2. 섹션 간 여백 (Section Spacing) +### 2-3. 카드/카드 그리드 (Card Spacing) +### 2-4. Border Radius / 컨테이너 + +## 3. 마이크로 인터랙션 (Micro Interaction) +### 3-1. Hover / Focus 효과 +### 3-2. Transition 패턴 +### 3-3. 레이어링 (z-index / position) + +## 4. 라이팅 톤앤매너 (Microcopy & Voice) +### 4-1. 헤드라인 / 서브헤드라인 / CTA 카피 +### 4-2. Placeholder 및 보이스 신호` + : part === 2 + ? ` +## 5. 정보 구조 / 사이트 맵 (Information Architecture) +### 5-1. 사이트 트리 다이어그램 (Page Tree) +- \`sitemap.asciiTree\`를 코드블록으로 그대로 옮겨 적을 것. +### 5-2. 페이지 목록 (Flat View) +### 5-3. 페이지별 구성 요약 (Page Composition) +### 5-4. IA 특징 정리 +### 5-5. 재구축용 컴포넌트 명세 (Component Reconstruction Spec) +### 5-6. 미디어 처리 (Media Treatment) + +## 6. 준비해야 할 리소스 (Resources You Need to Prepare) +### 6-1. 페이지별 이미지/비디오 수 +### 6-2. 카피라이팅 분량 +### 6-3. 폼/입력 필드 목록 + +## 7. 디자인 토큰 (Design Tokens) +- Color / Typography / Spacing / Radius / Border / Shadow / Motion 각각 표로 정리. + +## 8. 페이지 템플릿 맵 (Page Template Map) + +스캔된 페이지들의 \`primaryContentType\` + \`sectionRoles\` + \`h2List\`를 묶어 **반복되는 템플릿 유형**을 도출하고, 반드시 아래 표 형식으로 작성하라. **반드시 마크다운 표 문법을 쓸 것** (글머리표·산문 금지). 템플릿은 최소 3개 이상 도출하되, 페이지가 모두 다르면 그만큼만 작성. + +| 템플릿 ID | 적용 URL | 공통 블록 순서 (위 → 아래) | 페이지별 차이점 | 재사용 컴포넌트 | +|---|---|---|---|---| +| T1: Gallery Landing | / | Header → Hero(작품 1장) → 작품 그리드(3열) → Footer | (없음 / 단독 페이지) | Header, ImageCard, Footer | +| T2: Category List | /shop, /paintings | Header → 카테고리 타이틀(h1) → 작품 그리드(2열) → Pagination → Footer | 카테고리명·작품 수 다름 | Header, ImageCard, Footer, Pagination | +| T3: Detail | /shop/oil-painting/limited-editions | Header → Breadcrumb → 작품 이미지(좌) + 메타·CTA(우) → 관련 작품 → Footer | 상품별 이미지·가격·CTA 문구 다름 | Header, BreadcrumbBar, BuyButton, RelatedGrid, Footer | + +작성 규칙: +- **템플릿 ID**: \`T1: <역할>\` 형식. 역할은 한국어 또는 영어 모두 OK. +- **적용 URL**: 해당 템플릿을 쓰는 페이지의 URL을 콤마로 모두 나열. 1개면 1개만. +- **공통 블록 순서**: \`sectionRoles\`의 tag 순서를 기반으로 \`Header → Hero → ... → Footer\` 식으로 위→아래 흐름을 화살표(\`→\`)로 표기. +- **페이지별 차이점**: 같은 템플릿을 쓰는 페이지들 사이의 변하는 부분(타이틀/이미지 수/CTA 문구 등). 단독 페이지면 \`(없음 / 단독 페이지)\`. +- **재사용 컴포넌트**: 5-5에서 정의한 컴포넌트 이름을 콤마로 나열. + +표 아래에 각 템플릿을 ATag/CSS 명세 수준으로 풀어 쓰는 짧은 단락을 덧붙여도 좋다 (선택).` + : ` +## 9. 원본 사이트 재구축 명세 (Rebuild Spec — Same Site, Built From Scratch) + +> **⚠️ 이 단계의 미션 (절대 이탈 금지)** +> - 이 섹션은 **원본 레퍼런스 사이트와 가능한 한 같은 사이트를 처음부터 다시 만들기 위한 개발 명세**다. +> - 다른 서비스(대시보드, 분석 툴, SaaS 등)로 **재해석·확장·전환하지 말 것**. 사용자 컨텍스트가 원본과 다른 도메인이면 part 9에서는 무시한다. +> - "개선 / 재설계 / 모던화 / 데이터 시각화 추가" 같은 변형 제안 금지. **원본 그대로 복원**이 유일한 목적. +> - 모든 결정값(색상·폰트·여백·Radius·전환 속도)은 part 1~7에서 추출한 토큰을 그대로 인용한다. + +### 9-1. 디자인 토큰 정의 (원본 값 그대로) +- part 7에서 도출한 토큰을 CSS 변수 또는 Tailwind config 형식으로 코드블록에 옮긴다. 값은 절대 임의로 바꾸지 말 것. + +### 9-2. 컴포넌트 명세 (원본 사이트의 카드/버튼/네비 등) +- part 5-5의 컴포넌트별 props·치수·padding·radius·border·shadow를 코드블록 형태로 명세. + +### 9-3. 페이지별 레이아웃 마크업 가이드 +- part 8 페이지 템플릿 맵의 각 템플릿(T1, T2, ...)에 대해 HTML 골격(섹션 → 자식 컴포넌트)을 의사 JSX/HTML로 1개씩 제시. + +### 9-4. 인터랙션 재현 명세 +- part 3의 hover/focus/transition 값을 어느 컴포넌트에 어떻게 적용할지 명시 (예: \`.btn:hover { background: ...; transition: 0.2s ease; }\`). + +### 9-5. 콘텐츠 및 자산 준비 목록 +- part 6의 페이지별 이미지/비디오 수, 카피 분량, 폼 필드를 체크리스트로 정리. 사장님이 준비해야 할 자산 목록. + +### 9-6. 개발 티켓 (원본 복원 기준) +- 위 9-1 ~ 9-5를 구현 가능한 단위로 쪼개 \`[FE] / [BE] / [Asset]\` 태그를 붙여 티켓 형태로 나열. 모든 티켓은 "원본 사이트와 같게 만들기" 범위 안에 있어야 한다. 신규 기능 제안 금지. + +## 🔍 복원 시 추정이 필요한 영역 (Buildability Gaps) + +- 스캔으로는 잡히지 않는 영역(다이나믹 데이터·CMS 구조·실제 폰트 라이선스·결제 연동 등)을 나열. 추측이 필요한 부분만 적고, 임의로 결정하지 말 것. + +> **주의**: 이 단계는 새로운 서비스 기획이 아니라 **원본 사이트 그 자체를 다시 짓기 위한 시방서**다. 9-1 ~ 9-6의 모든 값은 part 1~8에서 인용한 수치여야 한다.`; + + const partGoal = part === 1 + ? '1/3단계: 원본 사이트의 시각·인터랙션·카피 톤을 4-렌즈로 분석한다.' + : part === 2 + ? '2/3단계: 원본의 IA, 페이지 템플릿, 디자인 토큰, 준비 리소스를 정리한다. 8단계 Page Template Map은 반드시 표 형식으로 작성한다.' + : '3/3단계: 원본 레퍼런스 사이트를 처음부터 다시 만들기 위한 개발 명세를 작성한다. **사용자 컨텍스트는 무시하고, 다른 서비스로 재해석하지 않는다.**'; + + return `당신은 시니어 UX/UI 분석가 겸 프론트엔드 아키텍트다. +${sharedRules} + +[이번 단계 목표] +${partGoal} + +[레퍼런스 사이트 스캔 데이터 (JSON)] +\`\`\`json +${JSON.stringify(slim, null, 2)} +\`\`\` + +[사용자 보조 컨텍스트 — part 1·2의 톤 추정에만 참고. part 3에서는 무시할 것.] +${userBlock} + +[작성할 보고서 섹션 (이 구조를 그대로 따를 것)] +${partTemplate}`; +} + +/** + * Bridge `/api/lm` 프록시로 OpenAI 호환 chat completion 1회 호출. + * LLM 서버/모델은 Astra 설정(g1nation.ollamaUrl / defaultModel)을 사용한다. + */ +async function callLmSynthesis(prompt: string): Promise { + const cfg = vscode.workspace.getConfiguration('g1nation'); + const lmUrl = (cfg.get('ollamaUrl', 'http://127.0.0.1:11434') || 'http://127.0.0.1:11434').replace(/\/$/, ''); + const model = (cfg.get('defaultModel', '') || 'gemma4:e2b').trim(); + // temperature는 설정값(g1nation.datacollectSynthesisTemperature). 낮을수록 환각· + // 깨진 문자가 줄어든다. 0~2 범위로 클램프, 기본 0.1. + const temperature = Math.max(0, Math.min(2, cfg.get('datacollectSynthesisTemperature', 0.1) ?? 0.1)); + const res = await bridgeFetch('/api/lm', { + method: 'POST', + body: JSON.stringify({ + url: `${lmUrl}/v1/chat/completions`, + payload: { + model, + messages: [ + { role: 'system', content: '당신은 시니어 UX/UI 분석가이자 프론트엔드 아키텍트다. 모든 보고서는 한국어로, 제공된 JSON 스캔 데이터의 구체적 수치를 인용해 작성한다. 원본 레퍼런스 사이트를 처음부터 다시 만들기 위한 명세를 작성하는 것이 미션이며, 다른 서비스로 재해석·확장하지 않는다.' }, + { role: 'user', content: prompt }, + ], + temperature, + }, + }), + }, { timeoutMs: 120_000 }); + const content = res?.choices?.[0]?.message?.content + ?? res?.choices?.[0]?.text + ?? res?.answer + ?? res?.response + ?? ''; + return String(content).trim(); +} + +async function runBenchmark(arg: string, view: Webview | undefined): Promise { + // 인자 파싱: 첫 비-옵션 토큰=URL, `depth=N`·`pages=N`=크롤 옵션, 나머지=보조 컨텍스트. + // 예) `/benchmark caliverse.io depth=2 pages=12 우리 랜딩 참고용` + // URL 토큰만 떼어내므로 "분석해줘" 같은 자연어가 섞여도 안전하다. + const tokens = arg.trim().split(/\s+/).filter(Boolean); + let url = ''; + let depthArg: number | undefined; + let pagesArg: number | undefined; + const restParts: string[] = []; + for (const t of tokens) { + const m = /^(depth|pages)=(\d+)$/i.exec(t); + if (m) { + if (m[1].toLowerCase() === 'depth') depthArg = Number(m[2]); + else pagesArg = Number(m[2]); + } else if (!url) { + url = t; + } else { + restParts.push(t); + } + } if (!url) { - chunk(view, `사용법: \`/benchmark \`\n예: \`/benchmark https://example.com\`\n`); + chunk(view, `사용법: \`/benchmark [depth=N] [pages=N] [보조 설명]\`\n예: \`/benchmark https://example.com depth=2 pages=12\`\n`); return true; } + const userContent = restParts.join(' '); - chunk(view, `🔍 **Web Benchmark 스캔**: ${url}\n(Playwright + 디자인 토큰/사이트맵 추출, 최대 8페이지)\n\n`); + // 크롤 옵션 우선순위: 명령 인자 > Settings 설정값 > 기본값(depth 1 / 8페이지). + const cfg = vscode.workspace.getConfiguration('g1nation'); + const crawlDepth = depthArg ?? (cfg.get('datacollectCrawlDepth', 1) ?? 1); + const maxPages = pagesArg ?? (cfg.get('datacollectMaxPages', 8) ?? 8); + + chunk(view, `🔍 **Web Benchmark 스캔**: ${url}\n(crawlDepth ${crawlDepth} · 최대 ${maxPages}페이지 · Playwright)\n\n⏳ 브라우저 기동 · 페이지 크롤링 · 디자인 토큰 추출 중…`); + + // 1) scan — 진행 중 멈춘 줄로 오해하지 않도록 4초마다 경과 시간을 누적 표시. + const t0 = Date.now(); + const heartbeat = setInterval(() => { + chunk(view, ` ·${Math.round((Date.now() - t0) / 1000)}s`); + }, 4000); 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 }), + body: JSON.stringify({ url, captureScreenshots: false, maxPages, crawlDepth }), }, { timeoutMs: 6 * 60_000 }, - ); - + ).finally(() => clearInterval(heartbeat)); 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✅ **스캔 완료** (${Math.round((Date.now() - t0) / 1000)}s · ${s?.sitemap?.totalPages ?? 1}페이지)\n\n`); - chunk(view, `### 디자인 토큰 (상위)\n`); + // 스캔이 에러 없이 끝나도(잘못된 URL·봇 차단·JS 렌더링 등) 알맹이가 빌 수 있다. + const looksEmpty = !s?.meta?.title + && !(s?.design?.colors?.palette?.length) + && !s?.microcopy?.headline; + if (looksEmpty) { + chunk(view, `⚠️ **스캔 결과가 비어 있습니다.** URL이 정확한지, 사이트가 접근 가능한지(봇 차단·JS 전용 렌더링 포함) 확인하세요. 스캔한 URL: \`${url}\`\n\n`); + } + + // raw 스캔 요약 — LLM 합성이 실패하거나 스캔이 비었을 때의 fallback 본문. 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`); + const rawReport = [ + `### 메타`, + `- **title**: ${s?.meta?.title || '(없음)'}`, + `- **description**: ${s?.meta?.description || '(없음)'}`, + `- **lang**: ${s?.meta?.lang || '(없음)'}`, + ``, + `### 디자인 토큰 (상위)`, + `- **컬러 팔레트 Top 5**: ${palette.map((p: any) => `\`${p.value}\` (×${p.count})`).join(', ') || '(없음)'}`, + `- **컬러 비율**: ${s?.design?.colors?.composition?.ratioLabel || '(없음)'}`, + `- **Primary Font**: \`${s?.design?.typography?.primaryFont || '(없음)'}\``, + ``, + `### 사이트맵 (${s?.sitemap?.totalPages ?? 1}페이지, depth ${s?.sitemap?.crawlDepth ?? 0})`, + '```', + s?.sitemap?.ascii || '(없음)', + '```', + ].join('\n'); - chunk(view, `### 사이트맵 (${s?.sitemap?.totalPages ?? 1}페이지, depth ${s?.sitemap?.crawlDepth ?? 0})\n`); - chunk(view, `\`\`\`\n${s?.sitemap?.ascii || '(없음)'}\n\`\`\`\n\n`); + // 2) synthesize — LLM 4-렌즈 합성 3단계 (Datacollect 웹앱과 동일한 리포트). + // 스캔이 비었거나 합성이 실패하면 raw 요약으로 fallback. + let finalReport: string; + if (looksEmpty) { + chunk(view, `(스캔이 비어 LLM 합성을 건너뜁니다.)\n\n`); + finalReport = rawReport; + } else { + const model = (cfg.get('defaultModel', '') || 'gemma4:e2b').trim(); + chunk(view, `🧪 **LLM 4-렌즈 합성** (3단계 · 모델 \`${model}\`)\n모델·하드웨어에 따라 수 분 걸릴 수 있습니다…\n`); + try { + const parts: string[] = []; + for (const part of [1, 2, 3] as const) { + chunk(view, `\n · 합성 ${part}/3 진행 중…`); + const partT0 = Date.now(); + const out = await callLmSynthesis(buildSynthesisPrompt(s, userContent, part)); + if (!out) throw new Error(`LLM ${part}/3 응답이 비어 있습니다.`); + parts.push(out); + chunk(view, ` ✓ (${Math.round((Date.now() - partT0) / 1000)}s)`); + } + finalReport = parts.join('\n\n---\n\n'); + chunk(view, `\n\n`); + } catch (e: any) { + chunk(view, `\n\n⚠️ LLM 합성 실패: ${e?.message || String(e)}\n원시 스캔 요약만 표시·저장합니다. (LM 서버가 떠 있는지, \`g1nation.ollamaUrl\` / \`defaultModel\` 설정을 확인하세요.)\n\n`); + finalReport = rawReport; + } + } - 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, finalReport + '\n\n'); + + // 3) save — 저장 위치 우선순위: g1nation.datacollectSavePath > Bridge WIKI_RAW_PATH. + // 어느 쪽이든 Astra 코드에는 절대경로가 하드코딩되지 않는다. + try { + const today = new Date().toISOString().slice(0, 10); + let host = url; + try { host = new URL(/^https?:\/\//i.test(url) ? url : `https://${url}`).host; } catch { /* keep raw url */ } + const title = `웹벤치마크 ${host} ${today}`; + const fileMarkdown = [ + `# ${title}`, + ``, + `- **원본 URL**: ${url}`, + `- **스캔 시각**: ${new Date().toISOString()}`, + `- **크롤 옵션**: depth ${crawlDepth} · 최대 ${maxPages}페이지`, + `- **생성**: Astra /benchmark · Datacollect web-benchmark`, + ``, + finalReport, + ``, + ].join('\n'); + const savePath = (cfg.get('datacollectSavePath', '') || '').trim(); + const body: Record = { title, content: fileMarkdown }; + if (savePath) body.saveDir = savePath; + const saved = await bridgeFetch<{ success: boolean; path?: string }>( + '/api/wiki/save', + { method: 'POST', body: JSON.stringify(body) }, + { timeoutMs: 30_000 }, + ); + chunk(view, `💾 **결과물 저장 완료**: \`${saved?.path || '(경로 미확인)'}\`\n`); + } catch (e: any) { + chunk(view, `⚠️ 결과물 저장 실패: ${e?.message || String(e)}\n`); + } - chunk(view, `> 💡 더 깊은 4-렌즈/Rebuild Blueprint 합성을 원하면 위 결과를 인용해 Astra에 추가 질문하세요.\n`); return true; } diff --git a/src/features/settings/settingsPanelProvider.ts b/src/features/settings/settingsPanelProvider.ts index 22019fa..a94447c 100644 --- a/src/features/settings/settingsPanelProvider.ts +++ b/src/features/settings/settingsPanelProvider.ts @@ -83,6 +83,14 @@ interface SettingsState { maxAutoSteps: number; maxContextSize: number; }; + datacollect: { + bridgeUrl: string; + /** Empty → results saved to the Bridge's WIKI_RAW_PATH default. */ + savePath: string; + crawlDepth: number; + maxPages: number; + synthesisTemperature: number; + }; google: { clientId: string; /** secret 자체는 client 에 echo 안 함 — *설정 여부* 만. true 면 input placeholder 가 "저장됨" 으로 바뀜. */ @@ -237,6 +245,9 @@ export class SettingsPanelProvider implements vscode.WebviewViewProvider { case 'advanced.update': await this._handleAdvancedUpdate(msg); return; + case 'datacollect.update': + await this._handleDatacollectUpdate(msg); + return; case 'google.update': await this._handleGoogleUpdate(msg); return; @@ -572,6 +583,28 @@ export class SettingsPanelProvider implements vscode.WebviewViewProvider { } } + // ────────────── Datacollect (slash 명령) ────────────── + // /research·/benchmark·/youtube 가 호출하는 Bridge URL 과, 결과물 저장 위치. + // savePath 가 비어 있으면 Bridge 의 WIKI_RAW_PATH 환경변수가 저장 위치를 결정한다. + + private async _handleDatacollectUpdate(msg: any): Promise { + if (typeof msg.bridgeUrl === 'string') { + await this._safeConfigUpdate('datacollectBridgeUrl', msg.bridgeUrl.trim()); + } + if (typeof msg.savePath === 'string') { + await this._safeConfigUpdate('datacollectSavePath', msg.savePath.trim()); + } + if (typeof msg.crawlDepth === 'number' && Number.isFinite(msg.crawlDepth)) { + await this._safeConfigUpdate('datacollectCrawlDepth', Math.max(0, Math.min(3, Math.floor(msg.crawlDepth)))); + } + if (typeof msg.maxPages === 'number' && Number.isFinite(msg.maxPages)) { + await this._safeConfigUpdate('datacollectMaxPages', Math.max(1, Math.min(20, Math.floor(msg.maxPages)))); + } + if (typeof msg.synthesisTemperature === 'number' && Number.isFinite(msg.synthesisTemperature)) { + await this._safeConfigUpdate('datacollectSynthesisTemperature', Math.max(0, Math.min(2, msg.synthesisTemperature))); + } + } + private async _refreshState(): Promise { if (!this._view && !this._panel) return; const cfg = vscode.workspace.getConfiguration('g1nation'); @@ -620,6 +653,13 @@ export class SettingsPanelProvider implements vscode.WebviewViewProvider { maxAutoSteps: cfg.get('maxAutoSteps', 50) ?? 50, maxContextSize: cfg.get('maxContextSize', 32000) ?? 32000, }, + datacollect: { + bridgeUrl: cfg.get('datacollectBridgeUrl', '') || '', + savePath: cfg.get('datacollectSavePath', '') || '', + crawlDepth: cfg.get('datacollectCrawlDepth', 1) ?? 1, + maxPages: cfg.get('datacollectMaxPages', 8) ?? 8, + synthesisTemperature: cfg.get('datacollectSynthesisTemperature', 0.1) ?? 0.1, + }, google: this._buildGoogleState(), providers: await this._buildProvidersState(), devilAgent: { enabled: cfg.get('devilAgent.enabled', false) }, diff --git a/src/sidebar/chatHandlers.ts b/src/sidebar/chatHandlers.ts index 9754db9..f543da7 100644 --- a/src/sidebar/chatHandlers.ts +++ b/src/sidebar/chatHandlers.ts @@ -16,24 +16,19 @@ export async function handleChatMessage(provider: SidebarChatProvider, data: any switch (data.type) { case 'prompt': case 'promptWithFile': + console.error(`[ASTRA-DEBUG] prompt case entered type=${data?.type} value=${JSON.stringify(String(data?.value ?? '').slice(0, 80))}`); provider._lmStudio?.activity.bump(); - await provider._context.globalState.update(SidebarChatProvider.blankChatStateKey, false); // ── 📻 Datacollect Radio (slash 명령) 우선 분기 ── - // 사용자가 채팅에서 `/research`, `/benchmark`, `/youtube`, `/blog` 같은 - // 슬래시 명령을 보내면 Datacollect bridge(3002)로 위임. 회사 모드/일반 - // chat 분기보다 먼저 잡아 LLM 토큰을 쓰지 않고 직접 처리한다. - // - // 진단 logging: "/benchmark 입력했는데 아무 답변도 안 옴" 같은 보고가 - // 들어왔을 때, OutputChannel(Astra)에 단계별 trace가 남으면 어디서 - // 막혔는지 (분기 진입 / view 부재 / bridge 호출 실패) 즉시 판별 가능. + // 주의: globalState.update보다 *먼저* 잡는다 — 글로벌 state가 ~1MB까지 + // 누적된 환경에서 update가 느려 첫 prompt가 hang하는 사례 보고됨. slash + // 명령은 LLM을 우회하니 blank chat state 갱신도 필요 없음. if (typeof data.value === 'string') { const { isSlashCommand, handleSlashCommand } = await import('../features/datacollect/slashRouter'); const matched = isSlashCommand(data.value); + console.error(`[ASTRA-DEBUG] slash check matched=${matched} hasView=${!!provider._view}`); logInfo(`[SLASH] prompt received: ${JSON.stringify(data.value).slice(0, 100)} matched=${matched} hasView=${!!provider._view}`); if (matched) { if (!provider._view?.webview) { - // webview가 비활성/닫힘 상태면 chunk가 silently drop되므로 - // 사용자가 아무 응답도 못 본다. notification으로 즉시 surface. const msg = '📻 Datacollect Radio: 채팅 webview가 활성 상태가 아닙니다. Astra 사이드바를 한 번 열고 다시 시도해 주세요.'; await vscode.window.showWarningMessage(msg); logInfo(`[SLASH] webview not available — aborting`); @@ -45,6 +40,7 @@ export async function handleChatMessage(provider: SidebarChatProvider, data: any return true; } } + await provider._context.globalState.update(SidebarChatProvider.blankChatStateKey, false); // ── 1인 기업 모드 우선 분기 ── // 회사 모드일 땐 들어온 메시지를 intent classifier에 한 번 통과시켜 // (a) 잡담/질문/짧은 응답 → 일반 채팅 경로, (b) 후속 대화 → 일반 채팅