feat(wiki): /wikify 포맷 정본 통일 + 채팅 URL 실데이터 주입 (v2.2.229)
[포맷 통일 — Datacollect 가 정본] /wikify 와 Datacollect research 가 각자 포맷 사본을 들고 어긋났던 문제. 더 최신인 Datacollect 포맷을 wiki_format.mjs 정본으로 추출(브리지 측)하고, /wikify 는 GET /api/wiki/template 로 받아 소비. 구버전 브리지면 내장 사본 fallback (정본 v3.1과 동일 내용). 포맷 수정은 이제 wiki_format.mjs 한 곳. /wikify 가 정본을 따르며 고쳐진 것: - category "10_Wiki/Topics"(물리 경로 버그) → 논리 도메인 규칙 - 고정 신뢰도 B/0.8 → 소스 평가 동적 부여 (충돌 신뢰도 권고의 입력 품질) - aliases 빈 배열 → 동의어 3-8개 강제 (어휘갭 검색 보완) - "## 🔗 관련 문서 링크" → "## 🔗 지식 그래프" + 고아 방지 up-link - 인라인 [S#] 출처 인용 + 📚 출처 섹션, 비교표·코드 패턴 조건 섹션 [채팅 URL 접근 — 강제 주입 패턴 4번째 적용] 일반 채팅에 URL 을 주면 "접근 불가"라고 답하던 공백: urlContext 가 URL 감지 시 브리지 /api/web-extract(기존 /wikify 인프라 재사용)로 본문 추출 → 컨텍스트 주입 (8K 캡, 잘림 시 /wikify 안내). 실패 시 정직 블록 (브리지 확인 안내 + 추측 금지). 슬래시 명령은 제외 (자체 처리). 주입 성공 로그 포함. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* URL 컨텍스트 빌더 — 채팅 프롬프트에 URL 이 있으면 본문을 추출해 실데이터로 주입.
|
||||
*
|
||||
* 문제: /wikify 는 URL 에 접근하지만(브리지 /api/web-extract), 일반 채팅에 URL 을
|
||||
* 주면 추출 경로가 없어 모델이 "접근할 수 없습니다"라고 답하거나 내용을 추측했다.
|
||||
*
|
||||
* 수정: 강제 주입 패턴의 4번째 적용 (일정→캘린더, 자기평가→인벤토리, 정정→캡처와
|
||||
* 동일 설계). 이미 검증된 브리지 추출 인프라를 재사용 — 새 크롤러 없음.
|
||||
* 실패 시(브리지 다운/추출 실패) 정직한 안내 블록 — 모델이 내용을 지어내지 않게.
|
||||
*/
|
||||
import { logInfo } from '../../utils';
|
||||
import { bridgeFetch, BRIDGE_API } from '../../features/datacollect/bridgeClient';
|
||||
|
||||
const URL_RE = /https?:\/\/[^\s<>"'`)\]]+/i;
|
||||
const MAX_BODY_CHARS = 8000;
|
||||
const EXTRACT_TIMEOUT_MS = 45_000;
|
||||
|
||||
/**
|
||||
* 프롬프트에서 추출 대상 URL 을 찾는다. 슬래시 명령(/wikify 등)은 자체 처리하므로
|
||||
* 제외. 없으면 null.
|
||||
*/
|
||||
export function extractUrlFromPrompt(prompt: string): string | null {
|
||||
const p = (prompt || '').trim();
|
||||
if (!p || p.startsWith('/')) return null;
|
||||
const m = URL_RE.exec(p);
|
||||
if (!m) return null;
|
||||
// 문장 끝 구두점이 URL 에 붙는 흔한 오염 제거.
|
||||
return m[0].replace(/[.,;:!?…」』)]+$/, '');
|
||||
}
|
||||
|
||||
/** URL 본문을 추출해 컨텍스트 블록 생성. 실패해도 throw 하지 않는다 (정직 블록 반환). */
|
||||
export async function buildUrlContext(url: string): Promise<string> {
|
||||
try {
|
||||
const data = await bridgeFetch<{ success: boolean; title?: string; description?: string; text?: string; textLength?: number; truncated?: boolean }>(
|
||||
BRIDGE_API.web.extract,
|
||||
{ method: 'POST', body: JSON.stringify({ url }) },
|
||||
{ timeoutMs: EXTRACT_TIMEOUT_MS },
|
||||
);
|
||||
const body = String(data?.text || '').slice(0, MAX_BODY_CHARS);
|
||||
if (!body.trim() || body.trim().length < 50) {
|
||||
return [
|
||||
`[URL CONTENT — ${url}]`,
|
||||
'상태: 본문 추출 실패 (콘텐츠 없음 또는 JS 전용 렌더링).',
|
||||
'→ 사용자에게 이 URL 의 본문을 가져오지 못했다고 정직하게 알리고, 내용을 추측해 답하지 마라.',
|
||||
].join('\n');
|
||||
}
|
||||
logInfo('URL 컨텍스트 주입.', { url, chars: body.length, truncated: !!data?.truncated });
|
||||
return [
|
||||
`[URL CONTENT — 실데이터 · ${url}]`,
|
||||
`제목: ${data?.title || '(없음)'}`,
|
||||
data?.description ? `설명: ${data.description}` : '',
|
||||
'아래 본문만 근거로 답하라. 본문에 없는 내용은 "본문에서 확인되지 않음"이라고 답하고 지어내지 마라.' +
|
||||
(body.length >= MAX_BODY_CHARS || data?.truncated ? ' (본문 일부 잘림 — 전체가 필요하면 /wikify 사용을 안내하라.)' : ''),
|
||||
'',
|
||||
body,
|
||||
].filter(Boolean).join('\n');
|
||||
} catch (e: any) {
|
||||
return [
|
||||
`[URL CONTENT — ${url}]`,
|
||||
`상태: 접근 실패 — ${String(e?.message ?? e).slice(0, 120)}`,
|
||||
'→ Datacollect 브리지(:3002)가 실행 중인지 확인하라고 사용자에게 안내하고, URL 내용을 추측해 답하지 마라.',
|
||||
].join('\n');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user