Update from Assistant
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": 1779084064068,
|
||||
"createdAt": 1779179392149,
|
||||
"modelVersion": "unknown"
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"result": "[CONFLICT WARNING] 성능이 200% 증가했습니다. vs 그러나 동시에 50% 감소했습니다. 최적화와 성능 저하가 동시에 발견됨.",
|
||||
"createdAt": 1779084064066,
|
||||
"createdAt": 1779179392147,
|
||||
"modelVersion": "unknown"
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"result": "Detailed Execution Plan: 1. Research 2. Analyze 3. Write report with high quality.",
|
||||
"createdAt": 1779084064063,
|
||||
"createdAt": 1779179392144,
|
||||
"modelVersion": "unknown"
|
||||
}
|
||||
+2
-2
@@ -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"
|
||||
}
|
||||
+8
-8
@@ -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": {
|
||||
@@ -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 \<url\>:** Playwright 기반 웹사이트 분석 — 디자인 토큰, 컬러 팔레트, 사이트맵 ASCII 다이어그램, 마이크로카피를 요약해 채팅에 표시.
|
||||
- **/youtube \<url\>:** 영상 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 모델을 직접 사용할 수 있습니다.
|
||||
|
||||
+6
-1
@@ -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,
|
||||
|
||||
@@ -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<string>('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<T = any>(
|
||||
path: string,
|
||||
init?: RequestInit,
|
||||
opts: BridgeFetchOptions = {},
|
||||
): Promise<T> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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> | 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<boolean> {
|
||||
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<boolean> {
|
||||
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<boolean> {
|
||||
if (!url) {
|
||||
chunk(view, `사용법: \`/benchmark <url>\`\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<boolean> {
|
||||
if (!url) {
|
||||
chunk(view, `사용법: \`/youtube <url>\`\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<boolean> {
|
||||
// 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;
|
||||
}
|
||||
@@ -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) 후속 대화 → 일반 채팅
|
||||
|
||||
Reference in New Issue
Block a user