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) <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"result": "Final report with inconsistencies. This should be long enough to pass validation.",
|
||||
"createdAt": 1779186936004,
|
||||
"createdAt": 1779250764072,
|
||||
"modelVersion": "unknown"
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"result": "[CONFLICT WARNING] 성능이 200% 증가했습니다. vs 그러나 동시에 50% 감소했습니다. 최적화와 성능 저하가 동시에 발견됨.",
|
||||
"createdAt": 1779186936000,
|
||||
"createdAt": 1779250764070,
|
||||
"modelVersion": "unknown"
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"result": "Detailed Execution Plan: 1. Research 2. Analyze 3. Write report with high quality.",
|
||||
"createdAt": 1779186935997,
|
||||
"createdAt": 1779250764068,
|
||||
"modelVersion": "unknown"
|
||||
}
|
||||
+2
-2
@@ -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"
|
||||
}
|
||||
+9
-9
@@ -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": {
|
||||
@@ -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 <url> depth=2 pages=12` 로 그때그때 덮어쓰기 가능 (우선순위: 명령어 > 설정 > 기본값).
|
||||
- **명령어 보조 컨텍스트.** `/benchmark <url> [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이 우리 코드를 가리는 케이스 발견용.
|
||||
|
||||
@@ -45,6 +45,51 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Datacollect -->
|
||||
<section class="section" data-section="datacollect">
|
||||
<h2>Datacollect (slash 명령)</h2>
|
||||
<p class="hint">채팅에서 <code>/research</code> · <code>/benchmark</code> · <code>/youtube</code> 를 입력하면 Datacollect Bridge로 위임됩니다. Bridge는 Datacollect 프로젝트에서 <code>npm run bridge</code> 로 실행해야 합니다.</p>
|
||||
<div class="row">
|
||||
<label for="dcBridgeUrl">Bridge URL</label>
|
||||
<div class="input-group">
|
||||
<input id="dcBridgeUrl" type="text" placeholder="http://127.0.0.1:3002" spellcheck="false" />
|
||||
<button data-save="datacollect.bridgeUrl">저장</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label for="dcSavePath">결과물 저장 폴더</label>
|
||||
<div class="input-group">
|
||||
<input id="dcSavePath" type="text" placeholder="(비우면 Bridge 기본 위치)" spellcheck="false" />
|
||||
<button data-save="datacollect.savePath">저장</button>
|
||||
</div>
|
||||
<small class="hint">/benchmark 등의 결과 markdown 저장 위치. 비워두면 Bridge의 <code>WIKI_RAW_PATH</code> 환경변수가 결정합니다 (코드에 절대경로 하드코딩 없음). 특정 폴더로 저장하려면 절대경로를 입력하세요.</small>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label for="dcCrawlDepth">크롤 깊이 기본값</label>
|
||||
<div class="input-group narrow">
|
||||
<input id="dcCrawlDepth" type="number" min="0" max="3" step="1" />
|
||||
<button data-save="datacollect.crawlDepth">저장</button>
|
||||
</div>
|
||||
<small class="hint">/benchmark 사이트맵 크롤 깊이. 0=루트만, 1=직속 자식, 2=손자, 3=깊은 크롤. 명령어에서 <code>depth=N</code> 으로 그때그때 덮어쓸 수 있습니다.</small>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label for="dcMaxPages">최대 페이지 기본값</label>
|
||||
<div class="input-group narrow">
|
||||
<input id="dcMaxPages" type="number" min="1" max="20" step="1" />
|
||||
<button data-save="datacollect.maxPages">저장</button>
|
||||
</div>
|
||||
<small class="hint">/benchmark 스캔 최대 페이지 수. 명령어에서 <code>pages=N</code> 으로 덮어쓸 수 있습니다 (Bridge 상한 20).</small>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label for="dcSynthTemp">합성 Temperature</label>
|
||||
<div class="input-group narrow">
|
||||
<input id="dcSynthTemp" type="number" min="0" max="2" step="0.05" />
|
||||
<button data-save="datacollect.synthesisTemperature">저장</button>
|
||||
</div>
|
||||
<small class="hint">/benchmark LLM 4-렌즈 합성의 temperature. 낮을수록(0.1) 환각·깨진 문자가 줄고 결정적입니다. 기본 0.1 권장.</small>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Memory -->
|
||||
<section class="section" data-section="memory">
|
||||
<h2>메모리</h2>
|
||||
|
||||
@@ -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;
|
||||
|
||||
+33
-7
@@ -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,
|
||||
|
||||
+6
-4
@@ -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)
|
||||
|
||||
@@ -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<bo
|
||||
|
||||
// ───────────────────────────── /benchmark ─────────────────────────────
|
||||
|
||||
async function runBenchmark(url: string, view: Webview | undefined): Promise<boolean> {
|
||||
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<string> {
|
||||
const cfg = vscode.workspace.getConfiguration('g1nation');
|
||||
const lmUrl = (cfg.get<string>('ollamaUrl', 'http://127.0.0.1:11434') || 'http://127.0.0.1:11434').replace(/\/$/, '');
|
||||
const model = (cfg.get<string>('defaultModel', '') || 'gemma4:e2b').trim();
|
||||
// temperature는 설정값(g1nation.datacollectSynthesisTemperature). 낮을수록 환각·
|
||||
// 깨진 문자가 줄어든다. 0~2 범위로 클램프, 기본 0.1.
|
||||
const temperature = Math.max(0, Math.min(2, cfg.get<number>('datacollectSynthesisTemperature', 0.1) ?? 0.1));
|
||||
const res = await bridgeFetch<any>('/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<boolean> {
|
||||
// 인자 파싱: 첫 비-옵션 토큰=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 <url>\`\n예: \`/benchmark https://example.com\`\n`);
|
||||
chunk(view, `사용법: \`/benchmark <url> [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<number>('datacollectCrawlDepth', 1) ?? 1);
|
||||
const maxPages = pagesArg ?? (cfg.get<number>('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<string>('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<string>('datacollectSavePath', '') || '').trim();
|
||||
const body: Record<string, unknown> = { 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
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<number>('maxAutoSteps', 50) ?? 50,
|
||||
maxContextSize: cfg.get<number>('maxContextSize', 32000) ?? 32000,
|
||||
},
|
||||
datacollect: {
|
||||
bridgeUrl: cfg.get<string>('datacollectBridgeUrl', '') || '',
|
||||
savePath: cfg.get<string>('datacollectSavePath', '') || '',
|
||||
crawlDepth: cfg.get<number>('datacollectCrawlDepth', 1) ?? 1,
|
||||
maxPages: cfg.get<number>('datacollectMaxPages', 8) ?? 8,
|
||||
synthesisTemperature: cfg.get<number>('datacollectSynthesisTemperature', 0.1) ?? 0.1,
|
||||
},
|
||||
google: this._buildGoogleState(),
|
||||
providers: await this._buildProvidersState(),
|
||||
devilAgent: { enabled: cfg.get<boolean>('devilAgent.enabled', false) },
|
||||
|
||||
@@ -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) 후속 대화 → 일반 채팅
|
||||
|
||||
Reference in New Issue
Block a user